doc2mcp 0.1.13

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.
Files changed (3) hide show
  1. package/README.md +62 -0
  2. package/dist/index.js +579 -0
  3. package/package.json +41 -0
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # doc2mcp CLI
2
+
3
+ Generate documentation MCP servers from your terminal.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g doc2mcp
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ # 1. Authorize (opens browser)
15
+ doc2mcp login
16
+
17
+ # 2. Convert docs → MCP (same pipeline as the website)
18
+ doc2mcp https://docs.example.com
19
+
20
+ # 3. Follow the install prompt for Cursor, VS Code, Claude, or Windsurf
21
+ ```
22
+
23
+ ## Commands
24
+
25
+ | Command | Description |
26
+ | --- | --- |
27
+ | `doc2mcp login` | Browser device authorization |
28
+ | `doc2mcp logout` | Remove stored credentials |
29
+ | `doc2mcp whoami` | Show logged-in user |
30
+ | `doc2mcp list` | List your MCP projects |
31
+ | `doc2mcp install <id>` | Install a ready MCP into editors |
32
+ | `doc2mcp <docs-url>` | Create MCP from documentation URL |
33
+
34
+ ## Environment
35
+
36
+ | Variable | Default | Description |
37
+ | --- | --- | --- |
38
+ | `DOC2MCP_API_URL` | `https://doc2mcp.com` | API base URL (use for local dev) |
39
+
40
+ Config is stored at `~/.doc2mcp/config.json`.
41
+
42
+ ## Local development
43
+
44
+ ```bash
45
+ cd cli
46
+ pnpm install
47
+ pnpm build
48
+ node dist/index.js --help
49
+
50
+ # Point at local Next app
51
+ DOC2MCP_API_URL=http://localhost:3000 node dist/index.js login
52
+ ```
53
+
54
+ ## Publish
55
+
56
+ ```bash
57
+ cd cli
58
+ pnpm build
59
+ npm publish --access public
60
+ ```
61
+
62
+ Ensure the npm package name `doc2mcp` is available before publishing.
package/dist/index.js ADDED
@@ -0,0 +1,579 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+
4
+ // src/index.ts
5
+ import { Command } from "commander";
6
+ import pc6 from "picocolors";
7
+
8
+ // src/commands/account.ts
9
+ import pc from "picocolors";
10
+
11
+ // src/store.ts
12
+ import { mkdir, readFile, writeFile } from "fs/promises";
13
+ import { dirname } from "path";
14
+
15
+ // src/config.ts
16
+ import { homedir } from "os";
17
+ import { join } from "path";
18
+ var DEFAULT_API_URL = "https://doc2mcp.com";
19
+ function getConfigPath() {
20
+ return join(homedir(), ".doc2mcp", "config.json");
21
+ }
22
+ function getApiUrl() {
23
+ return process.env.DOC2MCP_API_URL?.replace(/\/$/, "") ?? DEFAULT_API_URL;
24
+ }
25
+
26
+ // src/store.ts
27
+ async function loadConfig() {
28
+ try {
29
+ const raw = await readFile(getConfigPath(), "utf8");
30
+ const parsed = JSON.parse(raw);
31
+ return {
32
+ apiUrl: parsed.apiUrl || getApiUrl(),
33
+ token: parsed.token,
34
+ user: parsed.user
35
+ };
36
+ } catch {
37
+ return { apiUrl: getApiUrl() };
38
+ }
39
+ }
40
+ async function saveConfig(config) {
41
+ const path = getConfigPath();
42
+ await mkdir(dirname(path), { recursive: true });
43
+ await writeFile(path, `${JSON.stringify(config, null, 2)}
44
+ `, "utf8");
45
+ }
46
+ async function clearConfigToken() {
47
+ const config = await loadConfig();
48
+ await saveConfig({
49
+ apiUrl: config.apiUrl
50
+ });
51
+ }
52
+
53
+ // src/commands/account.ts
54
+ async function runLogout() {
55
+ await clearConfigToken();
56
+ process.stdout.write(`${pc.green("Logged out.")}
57
+ `);
58
+ }
59
+ async function runWhoami() {
60
+ const config = await loadConfig();
61
+ if (!config.token || !config.user) {
62
+ process.stdout.write(`${pc.yellow("Not logged in.")} Run ${pc.bold("doc2mcp login")}
63
+ `);
64
+ process.exitCode = 1;
65
+ return;
66
+ }
67
+ process.stdout.write(`${pc.bold(config.user.email)}
68
+ `);
69
+ if (config.user.name) {
70
+ process.stdout.write(`${pc.dim(config.user.name)}
71
+ `);
72
+ }
73
+ process.stdout.write(`${pc.dim(`API: ${config.apiUrl}`)}
74
+ `);
75
+ }
76
+
77
+ // src/commands/convert.ts
78
+ import pc5 from "picocolors";
79
+
80
+ // src/api.ts
81
+ import pc2 from "picocolors";
82
+ var ApiError = class extends Error {
83
+ status;
84
+ body;
85
+ constructor(message, status, body) {
86
+ super(message);
87
+ this.status = status;
88
+ this.body = body;
89
+ }
90
+ };
91
+ async function parseJson(response) {
92
+ const text = await response.text();
93
+ if (!text) {
94
+ return null;
95
+ }
96
+ try {
97
+ return JSON.parse(text);
98
+ } catch {
99
+ return { raw: text };
100
+ }
101
+ }
102
+ async function apiFetch(path, options = {}) {
103
+ const config = await loadConfig();
104
+ const baseUrl = config.apiUrl || getApiUrl();
105
+ const headers = new Headers(options.headers);
106
+ headers.set("Content-Type", "application/json");
107
+ const needsAuth = options.auth !== false;
108
+ if (needsAuth) {
109
+ if (!config.token) {
110
+ throw new ApiError("Not logged in. Run: doc2mcp login", 401, null);
111
+ }
112
+ headers.set("Authorization", `Bearer ${config.token}`);
113
+ }
114
+ const response = await fetch(`${baseUrl}${path}`, {
115
+ ...options,
116
+ headers
117
+ });
118
+ const body = await parseJson(response);
119
+ if (!response.ok) {
120
+ const message = typeof body === "object" && body !== null && "message" in body && typeof body.message === "string" ? body.message : `Request failed (${response.status})`;
121
+ throw new ApiError(message, response.status, body);
122
+ }
123
+ return body;
124
+ }
125
+ function printError(error) {
126
+ if (error instanceof ApiError) {
127
+ process.stderr.write(`${pc2.red("Error:")} ${error.message}
128
+ `);
129
+ return;
130
+ }
131
+ if (error instanceof Error) {
132
+ process.stderr.write(`${pc2.red("Error:")} ${error.message}
133
+ `);
134
+ return;
135
+ }
136
+ process.stderr.write(`${pc2.red("Error:")} Unknown error
137
+ `);
138
+ }
139
+
140
+ // src/commands/login.ts
141
+ import open from "open";
142
+ import ora from "ora";
143
+ import pc3 from "picocolors";
144
+ function sleep(ms) {
145
+ return new Promise((resolve) => {
146
+ setTimeout(resolve, ms);
147
+ });
148
+ }
149
+ async function runLogin() {
150
+ const config = await loadConfig();
151
+ const spinner = ora("Starting device authorization\u2026").start();
152
+ try {
153
+ const start = await apiFetch("/api/cli/auth/start", {
154
+ method: "POST",
155
+ auth: false,
156
+ body: JSON.stringify({})
157
+ });
158
+ spinner.stop();
159
+ process.stdout.write(
160
+ `
161
+ ${pc3.cyan("Open this link to authorize:")}
162
+ ${pc3.bold(start.verifyUrl)}
163
+
164
+ `
165
+ );
166
+ process.stdout.write(
167
+ `${pc3.dim("Code:")} ${pc3.bold(start.userCode)} ${pc3.dim("(also shown in browser)")}
168
+
169
+ `
170
+ );
171
+ try {
172
+ await open(start.verifyUrl);
173
+ } catch {
174
+ process.stdout.write(
175
+ `${pc3.yellow("Could not auto-open browser. Open the link manually.")}
176
+
177
+ `
178
+ );
179
+ }
180
+ const pollSpinner = ora("Waiting for approval\u2026").start();
181
+ const deadline = Date.now() + start.expiresIn * 1e3;
182
+ const intervalMs = start.interval * 1e3;
183
+ while (Date.now() < deadline) {
184
+ await sleep(intervalMs);
185
+ const poll = await apiFetch("/api/cli/auth/poll", {
186
+ method: "POST",
187
+ auth: false,
188
+ body: JSON.stringify({ deviceCode: start.deviceCode })
189
+ });
190
+ if (poll.status === "pending") {
191
+ continue;
192
+ }
193
+ if (poll.status === "approved") {
194
+ pollSpinner.succeed("Authorized");
195
+ await saveConfig({
196
+ apiUrl: config.apiUrl || getApiUrl(),
197
+ token: poll.token,
198
+ user: poll.user
199
+ });
200
+ process.stdout.write(
201
+ `${pc3.green("Logged in as")} ${poll.user.email}
202
+ `
203
+ );
204
+ return;
205
+ }
206
+ if (poll.status === "denied") {
207
+ pollSpinner.fail("Authorization denied");
208
+ process.exitCode = 1;
209
+ return;
210
+ }
211
+ pollSpinner.fail("Authorization expired");
212
+ process.exitCode = 1;
213
+ return;
214
+ }
215
+ pollSpinner.fail("Authorization timed out");
216
+ process.exitCode = 1;
217
+ } catch (error) {
218
+ spinner.fail("Login failed");
219
+ printError(error);
220
+ process.exitCode = 1;
221
+ }
222
+ }
223
+ async function ensureLoggedIn() {
224
+ const config = await loadConfig();
225
+ if (!config.token) {
226
+ await runLogin();
227
+ const next = await loadConfig();
228
+ if (!next.token) {
229
+ throw new Error("Login required");
230
+ }
231
+ }
232
+ }
233
+
234
+ // src/commands/install.ts
235
+ import { confirm, multiselect } from "@clack/prompts";
236
+ import pc4 from "picocolors";
237
+
238
+ // src/installers/detect.ts
239
+ import { access } from "fs/promises";
240
+ import { homedir as homedir2 } from "os";
241
+ import { join as join2 } from "path";
242
+ async function exists(path) {
243
+ try {
244
+ await access(path);
245
+ return true;
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+ async function detectClients() {
251
+ const home = homedir2();
252
+ const detected = [];
253
+ const cursorGlobal = join2(home, ".cursor", "mcp.json");
254
+ const cursorProject = join2(process.cwd(), ".cursor", "mcp.json");
255
+ if (await exists(cursorGlobal) || await exists(join2(home, ".cursor"))) {
256
+ detected.push({
257
+ id: "cursor",
258
+ label: "Cursor (global ~/.cursor/mcp.json)",
259
+ configPath: cursorGlobal
260
+ });
261
+ } else if (await exists(cursorProject)) {
262
+ detected.push({
263
+ id: "cursor",
264
+ label: "Cursor (project .cursor/mcp.json)",
265
+ configPath: cursorProject
266
+ });
267
+ } else {
268
+ detected.push({
269
+ id: "cursor",
270
+ label: "Cursor (~/.cursor/mcp.json)",
271
+ configPath: cursorGlobal
272
+ });
273
+ }
274
+ const vscodeProject = join2(process.cwd(), ".vscode", "mcp.json");
275
+ detected.push({
276
+ id: "vscode",
277
+ label: "VS Code (workspace .vscode/mcp.json)",
278
+ configPath: vscodeProject
279
+ });
280
+ const windsurf = join2(home, ".codeium", "windsurf", "mcp_config.json");
281
+ if (await exists(windsurf) || await exists(join2(home, ".codeium"))) {
282
+ detected.push({
283
+ id: "windsurf",
284
+ label: "Windsurf",
285
+ configPath: windsurf
286
+ });
287
+ }
288
+ const claudeMac = join2(
289
+ home,
290
+ "Library",
291
+ "Application Support",
292
+ "Claude",
293
+ "claude_desktop_config.json"
294
+ );
295
+ const claudeWin = join2(
296
+ home,
297
+ "AppData",
298
+ "Roaming",
299
+ "Claude",
300
+ "claude_desktop_config.json"
301
+ );
302
+ let claudePath = join2(home, ".config", "Claude", "claude_desktop_config.json");
303
+ if (process.platform === "win32") {
304
+ claudePath = claudeWin;
305
+ } else if (process.platform === "darwin") {
306
+ claudePath = claudeMac;
307
+ }
308
+ if (await exists(claudePath) || process.platform === "darwin") {
309
+ detected.push({
310
+ id: "claude",
311
+ label: "Claude Desktop",
312
+ configPath: claudePath
313
+ });
314
+ }
315
+ return detected;
316
+ }
317
+
318
+ // src/installers/merge.ts
319
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
320
+ import { dirname as dirname2 } from "path";
321
+ async function readJsonFile(path) {
322
+ try {
323
+ const raw = await readFile2(path, "utf8");
324
+ const parsed = JSON.parse(raw);
325
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
326
+ return parsed;
327
+ }
328
+ return {};
329
+ } catch {
330
+ return {};
331
+ }
332
+ }
333
+ async function writeJsonFile(path, value) {
334
+ await mkdir2(dirname2(path), { recursive: true });
335
+ await writeFile2(path, `${JSON.stringify(value, null, 2)}
336
+ `, "utf8");
337
+ }
338
+ function mergeMcpServers(existing, incoming) {
339
+ const currentServers = existing.mcpServers && typeof existing.mcpServers === "object" ? existing.mcpServers : {};
340
+ const incomingServers = incoming.mcpServers && typeof incoming.mcpServers === "object" ? incoming.mcpServers : incoming;
341
+ return {
342
+ ...existing,
343
+ mcpServers: {
344
+ ...currentServers,
345
+ ...incomingServers
346
+ }
347
+ };
348
+ }
349
+ function mergeVscodeMcp(existing, incoming) {
350
+ const currentServers = existing.servers && typeof existing.servers === "object" ? existing.servers : {};
351
+ const incomingServers = incoming.servers && typeof incoming.servers === "object" ? incoming.servers : {};
352
+ return {
353
+ ...existing,
354
+ servers: {
355
+ ...currentServers,
356
+ ...incomingServers
357
+ }
358
+ };
359
+ }
360
+
361
+ // src/installers/install.ts
362
+ async function installToClient(client, configPath, payload) {
363
+ if (client === "cursor") {
364
+ const existing = await readJsonFile(configPath);
365
+ const merged = mergeMcpServers(existing, payload.cursor);
366
+ await writeJsonFile(configPath, merged);
367
+ return;
368
+ }
369
+ if (client === "vscode") {
370
+ const existing = await readJsonFile(configPath);
371
+ const merged = mergeVscodeMcp(existing, payload.vscode);
372
+ await writeJsonFile(configPath, merged);
373
+ return;
374
+ }
375
+ if (client === "windsurf") {
376
+ const existing = await readJsonFile(configPath);
377
+ const merged = mergeMcpServers(existing, payload.windsurf);
378
+ await writeJsonFile(configPath, merged);
379
+ return;
380
+ }
381
+ if (client === "claude") {
382
+ const existing = await readJsonFile(configPath);
383
+ const merged = mergeMcpServers(existing, payload.claude);
384
+ await writeJsonFile(configPath, merged);
385
+ }
386
+ }
387
+
388
+ // src/commands/install.ts
389
+ async function promptInstall(install) {
390
+ const shouldInstall = await confirm({
391
+ message: "Install this MCP into your editor?",
392
+ initialValue: true
393
+ });
394
+ if (shouldInstall !== true) {
395
+ return;
396
+ }
397
+ const clients = await detectClients();
398
+ const selected = await multiselect({
399
+ message: "Select clients to install into",
400
+ options: clients.map((client) => ({
401
+ value: client.id,
402
+ label: client.label
403
+ })),
404
+ required: true
405
+ });
406
+ if (typeof selected === "symbol") {
407
+ return;
408
+ }
409
+ for (const clientId of selected) {
410
+ const client = clients.find((item) => item.id === clientId);
411
+ if (!client) {
412
+ continue;
413
+ }
414
+ await installToClient(client.id, client.configPath, install);
415
+ process.stdout.write(
416
+ `${pc4.green("Installed")} ${client.label}
417
+ ${pc4.dim(client.configPath)}
418
+ `
419
+ );
420
+ }
421
+ }
422
+ async function runInstallCommand(projectId) {
423
+ try {
424
+ await ensureLoggedIn();
425
+ const detail = await apiFetch(`/api/cli/projects/${projectId}`);
426
+ if (!detail.install) {
427
+ process.stderr.write(`${pc4.red("Project is not ready or missing install bundle.")}
428
+ `);
429
+ process.exitCode = 1;
430
+ return;
431
+ }
432
+ await promptInstall(detail.install);
433
+ } catch (error) {
434
+ printError(error);
435
+ process.exitCode = 1;
436
+ }
437
+ }
438
+
439
+ // src/commands/convert.ts
440
+ function sleep2(ms) {
441
+ return new Promise((resolve) => {
442
+ setTimeout(resolve, ms);
443
+ });
444
+ }
445
+ function printStatus(detail) {
446
+ const { project } = detail;
447
+ process.stdout.write(
448
+ `\r${pc5.cyan("Status:")} ${project.status.padEnd(12)} ${pc5.dim(project.name)}`
449
+ );
450
+ }
451
+ async function runConvert(sourceUrl) {
452
+ try {
453
+ await ensureLoggedIn();
454
+ process.stdout.write(`${pc5.bold("Converting")} ${sourceUrl}
455
+ `);
456
+ const created = await apiFetch("/api/cli/convert", {
457
+ method: "POST",
458
+ body: JSON.stringify({ sourceUrl })
459
+ });
460
+ process.stdout.write(`${pc5.dim("Project:")} ${created.id}
461
+ `);
462
+ let delayMs = 2e3;
463
+ const terminal = /* @__PURE__ */ new Set(["ready", "error"]);
464
+ while (true) {
465
+ const detail = await apiFetch(
466
+ `/api/cli/projects/${created.id}`
467
+ );
468
+ printStatus(detail);
469
+ if (terminal.has(detail.project.status)) {
470
+ process.stdout.write("\n");
471
+ break;
472
+ }
473
+ await sleep2(delayMs);
474
+ delayMs = Math.min(delayMs + 1e3, 1e4);
475
+ }
476
+ const finalDetail = await apiFetch(
477
+ `/api/cli/projects/${created.id}`
478
+ );
479
+ if (finalDetail.project.status === "error") {
480
+ const lastLog = finalDetail.project.logs.at(-1);
481
+ process.stderr.write(
482
+ `${pc5.red("Conversion failed.")}${lastLog ? ` ${lastLog.message}` : ""}
483
+ `
484
+ );
485
+ process.exitCode = 1;
486
+ return;
487
+ }
488
+ if (!finalDetail.mcp || !finalDetail.install) {
489
+ process.stderr.write(`${pc5.red("MCP ready but missing install bundle.")}
490
+ `);
491
+ process.exitCode = 1;
492
+ return;
493
+ }
494
+ process.stdout.write(`
495
+ ${pc5.green("MCP ready")}
496
+ `);
497
+ process.stdout.write(`${pc5.bold("Server:")} ${finalDetail.mcp.serverName}
498
+ `);
499
+ process.stdout.write(`${pc5.bold("URL:")} ${finalDetail.mcp.url}
500
+ `);
501
+ process.stdout.write(`${pc5.bold("Token:")} ${finalDetail.mcp.token}
502
+ `);
503
+ process.stdout.write(
504
+ `${pc5.dim("Also listed in the doc2mcp marketplace when ready.")}
505
+ `
506
+ );
507
+ await promptInstall(finalDetail.install);
508
+ } catch (error) {
509
+ printError(error);
510
+ process.exitCode = 1;
511
+ }
512
+ }
513
+ async function runList() {
514
+ try {
515
+ await ensureLoggedIn();
516
+ const data = await apiFetch("/api/cli/projects");
517
+ if (data.projects.length === 0) {
518
+ process.stdout.write(`${pc5.dim("No projects yet.")}
519
+ `);
520
+ return;
521
+ }
522
+ for (const project of data.projects) {
523
+ process.stdout.write(
524
+ `${pc5.bold(project.name)} ${pc5.dim(`[${project.status}]`)} ${project.source}
525
+ `
526
+ );
527
+ process.stdout.write(` ${pc5.dim(project.id)} ${project.sourceUrl ?? ""}
528
+ `);
529
+ }
530
+ } catch (error) {
531
+ printError(error);
532
+ process.exitCode = 1;
533
+ }
534
+ }
535
+
536
+ // src/index.ts
537
+ var program = new Command();
538
+ program.name("doc2mcp").description("Generate documentation MCP servers from your terminal").version("0.1.0");
539
+ program.command("login").description("Authorize the CLI via browser").action(async () => {
540
+ await runLogin();
541
+ });
542
+ program.command("logout").description("Remove stored credentials").action(async () => {
543
+ await runLogout();
544
+ });
545
+ program.command("whoami").description("Show the logged-in user").action(async () => {
546
+ await runWhoami();
547
+ });
548
+ program.command("list").description("List your MCP projects").action(async () => {
549
+ await runList();
550
+ });
551
+ program.command("install <projectId>").description("Install an existing MCP into Cursor, VS Code, Claude, or Windsurf").action(async (projectId) => {
552
+ await runInstallCommand(projectId);
553
+ });
554
+ program.argument("[url]", "Documentation URL to convert").action(async (url) => {
555
+ if (!url) {
556
+ program.help();
557
+ return;
558
+ }
559
+ try {
560
+ const parsed = new URL(url);
561
+ if (!["http:", "https:"].includes(parsed.protocol)) {
562
+ throw new Error("URL must start with http:// or https://");
563
+ }
564
+ } catch {
565
+ process.stderr.write(
566
+ `${pc6.red("Error:")} Invalid URL. Example: doc2mcp https://docs.example.com
567
+ `
568
+ );
569
+ process.exitCode = 1;
570
+ return;
571
+ }
572
+ await runConvert(url);
573
+ });
574
+ program.parseAsync(process.argv).catch((error) => {
575
+ const message = error instanceof Error ? error.message : "Unknown error";
576
+ process.stderr.write(`${pc6.red("Error:")} ${message}
577
+ `);
578
+ process.exit(1);
579
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "doc2mcp",
3
+ "version": "0.1.13",
4
+ "description": "Generate documentation MCP servers from your terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "doc2mcp": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "dev": "tsup --watch",
19
+ "prepublishOnly": "pnpm build"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "documentation",
24
+ "cli",
25
+ "cursor",
26
+ "model-context-protocol"
27
+ ],
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@clack/prompts": "^0.10.1",
31
+ "commander": "^13.1.0",
32
+ "open": "^10.1.0",
33
+ "ora": "^8.2.0",
34
+ "picocolors": "^1.1.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^22.13.10",
38
+ "tsup": "^8.4.0",
39
+ "typescript": "^5.8.2"
40
+ }
41
+ }