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.
- package/README.md +62 -0
- package/dist/index.js +579 -0
- 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
|
+
}
|