@docwhispr/mcp-server 0.1.0 → 0.1.3
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 +44 -42
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +139 -0
- package/dist/index.js +61 -141
- package/dist/lib.d.ts +70 -0
- package/dist/lib.js +237 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,25 +1,48 @@
|
|
|
1
1
|
# @docwhispr/mcp-server
|
|
2
2
|
|
|
3
|
-
Thin
|
|
3
|
+
Thin client for **docwhispr** cloud curriculum compiler (HiliFlow).
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
- Metered: `hiliflow_compile_episode` / `hiliflow_compile_series` (charges from the same monthly video pool as the web app)
|
|
7
|
-
- Upsell: `hiliflow_open_studio` → hosted interactive player URL
|
|
5
|
+
## One-time setup (paste from Settings)
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npx -y @docwhispr/mcp-server configure --key "hl_live_…"
|
|
9
|
+
# optional, Cursor only:
|
|
10
|
+
# npx -y @docwhispr/mcp-server configure --key "hl_live_…" --cursor-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Saves `~/.docwhispr/credentials.json`. CLI and any MCP host that runs this package will read it automatically. `--cursor-mcp` only updates `~/.cursor/mcp.json` (still **without** embedding the key).
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
## CLI
|
|
12
16
|
|
|
13
17
|
```bash
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
npx -y @docwhispr/mcp-server plan \
|
|
19
|
+
--pdf "/path/to/book.pdf" \
|
|
20
|
+
--pages 602-604 \
|
|
21
|
+
--topics EXTEST,INTEST
|
|
22
|
+
|
|
23
|
+
npx -y @docwhispr/mcp-server compile \
|
|
24
|
+
--pdf "/path/to/book.pdf" \
|
|
25
|
+
--pages 602-604 \
|
|
26
|
+
--topics EXTEST,INTEST \
|
|
27
|
+
--lang zh \
|
|
28
|
+
--character chr_pixel_voxel \
|
|
29
|
+
--wait
|
|
18
30
|
```
|
|
19
31
|
|
|
20
|
-
|
|
32
|
+
| Command | Credits |
|
|
33
|
+
| --- | ---: |
|
|
34
|
+
| `configure` | 0 |
|
|
35
|
+
| `plan` | 0 |
|
|
36
|
+
| `compile` | 1 |
|
|
37
|
+
| `credits` / `status` | 0 |
|
|
38
|
+
|
|
39
|
+
Auth: `HILIFLOW_API_KEY` env **or** credentials file from `configure`.
|
|
21
40
|
|
|
22
|
-
|
|
41
|
+
`402` = out of credits · `409` = another job already generating.
|
|
42
|
+
|
|
43
|
+
## MCP stdio
|
|
44
|
+
|
|
45
|
+
After `configure --cursor-mcp`, or manually:
|
|
23
46
|
|
|
24
47
|
```json
|
|
25
48
|
{
|
|
@@ -28,7 +51,6 @@ Save the returned `apiKey` (`hl_live_…`) once.
|
|
|
28
51
|
"command": "npx",
|
|
29
52
|
"args": ["-y", "@docwhispr/mcp-server"],
|
|
30
53
|
"env": {
|
|
31
|
-
"HILIFLOW_API_KEY": "hl_live_…",
|
|
32
54
|
"HILIFLOW_API_BASE": "https://docwhispr.fly.dev",
|
|
33
55
|
"HILIFLOW_WEB_BASE": "https://docwhispr.com"
|
|
34
56
|
}
|
|
@@ -37,36 +59,16 @@ Save the returned `apiKey` (`hl_live_…`) once.
|
|
|
37
59
|
}
|
|
38
60
|
```
|
|
39
61
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"cwd": "<repo-root>",
|
|
49
|
-
"env": {
|
|
50
|
-
"HILIFLOW_API_KEY": "hl_live_…",
|
|
51
|
-
"HILIFLOW_API_BASE": "http://127.0.0.1:8080"
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Tools
|
|
59
|
-
|
|
60
|
-
| Tool | Credits | Purpose |
|
|
61
|
-
| --- | ---: | --- |
|
|
62
|
-
| `hiliflow_credits` | 0 | Balance / monthly allowance |
|
|
63
|
-
| `hiliflow_plan_curriculum` | 0 | Book↔PDF resolve, keep/drop topics, episode split |
|
|
64
|
-
| `hiliflow_compile_episode` | 1 | Single project job |
|
|
65
|
-
| `hiliflow_compile_series` | N | Multi-episode split & enqueue |
|
|
66
|
-
| `hiliflow_job_status` | 0 | Poll job |
|
|
67
|
-
| `hiliflow_open_studio` | 0 | Hosted interactive studio URL |
|
|
62
|
+
| Tool | Credits |
|
|
63
|
+
| --- | ---: |
|
|
64
|
+
| `hiliflow_credits` | 0 |
|
|
65
|
+
| `hiliflow_plan_curriculum` | 0 |
|
|
66
|
+
| `hiliflow_compile_episode` | 1 |
|
|
67
|
+
| `hiliflow_compile_series` | N |
|
|
68
|
+
| `hiliflow_job_status` | 0 |
|
|
69
|
+
| `hiliflow_open_studio` | 0 |
|
|
68
70
|
|
|
69
|
-
## Monthly credits
|
|
71
|
+
## Monthly credits
|
|
70
72
|
|
|
71
73
|
| Plan | Episodes / month |
|
|
72
74
|
| --- | ---: |
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function run(argv: string[]): Promise<void>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI handlers for configure / plan / compile / credits / status.
|
|
3
|
+
* Invoked from index.ts when argv includes a subcommand.
|
|
4
|
+
*/
|
|
5
|
+
import { api, compileEpisode, ensureCursorMcpConfig, getWebBase, jobStatus, parsePages, parseTopics, planCurriculum, saveCredentials } from "./lib.js";
|
|
6
|
+
function usage(exitCode = 1) {
|
|
7
|
+
console.error(`Usage:
|
|
8
|
+
npx @docwhispr/mcp-server configure --key hl_live_… [--cursor-mcp]
|
|
9
|
+
npx @docwhispr/mcp-server plan --pdf <path> --pages <min>-<max> [--topics a,b] [--title ...]
|
|
10
|
+
npx @docwhispr/mcp-server compile --pdf <path> --pages <min>-<max> [--topics a,b] [--lang zh]
|
|
11
|
+
[--character chr_pixel_voxel] [--title ...] [--plan free|pro|pro_plus]
|
|
12
|
+
[--wait] [--poll-ms 20000]
|
|
13
|
+
npx @docwhispr/mcp-server credits [--plan free|pro|pro_plus]
|
|
14
|
+
npx @docwhispr/mcp-server status --job <id>
|
|
15
|
+
|
|
16
|
+
Auth: HILIFLOW_API_KEY env, or ~/.docwhispr/credentials.json from \`configure\`
|
|
17
|
+
`);
|
|
18
|
+
process.exit(exitCode);
|
|
19
|
+
}
|
|
20
|
+
function flag(args, name) {
|
|
21
|
+
const i = args.indexOf(name);
|
|
22
|
+
if (i < 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
return args[i + 1];
|
|
25
|
+
}
|
|
26
|
+
function has(args, name) {
|
|
27
|
+
return args.includes(name);
|
|
28
|
+
}
|
|
29
|
+
async function sleep(ms) {
|
|
30
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
31
|
+
}
|
|
32
|
+
async function waitForJob(jobId, pollMs) {
|
|
33
|
+
for (;;) {
|
|
34
|
+
const job = await jobStatus(jobId, "cli");
|
|
35
|
+
const status = String(job.status || "");
|
|
36
|
+
const progress = job.progressMessage ? ` · ${job.progressMessage}` : "";
|
|
37
|
+
console.error(`[status] ${status}${progress}`);
|
|
38
|
+
if (status === "succeeded" || status === "failed" || status === "error") {
|
|
39
|
+
return job;
|
|
40
|
+
}
|
|
41
|
+
await sleep(pollMs);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function run(argv) {
|
|
45
|
+
const [cmd, ...rest] = argv;
|
|
46
|
+
if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help")
|
|
47
|
+
usage(0);
|
|
48
|
+
try {
|
|
49
|
+
if (cmd === "configure") {
|
|
50
|
+
const key = flag(rest, "--key");
|
|
51
|
+
if (!key)
|
|
52
|
+
usage();
|
|
53
|
+
const path = saveCredentials({ apiKey: key });
|
|
54
|
+
const out = {
|
|
55
|
+
credentialsPath: path,
|
|
56
|
+
note: "API key saved. plan/compile/MCP will read it automatically."
|
|
57
|
+
};
|
|
58
|
+
if (has(rest, "--cursor-mcp")) {
|
|
59
|
+
out.cursorMcpPath = ensureCursorMcpConfig();
|
|
60
|
+
out.cursorNote =
|
|
61
|
+
"Cursor mcp.json updated (no key embedded). Restart MCP docwhispr in Cursor.";
|
|
62
|
+
}
|
|
63
|
+
console.log(JSON.stringify(out, null, 2));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (cmd === "credits") {
|
|
67
|
+
const plan = flag(rest, "--plan");
|
|
68
|
+
const q = plan ? `?plan=${encodeURIComponent(plan)}` : "";
|
|
69
|
+
const data = await api(`/api/mcp/credits${q}`, {}, "cli");
|
|
70
|
+
console.log(JSON.stringify(data, null, 2));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (cmd === "status") {
|
|
74
|
+
const jobId = flag(rest, "--job");
|
|
75
|
+
if (!jobId)
|
|
76
|
+
usage();
|
|
77
|
+
console.log(JSON.stringify(await jobStatus(jobId, "cli"), null, 2));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (cmd === "plan" || cmd === "compile") {
|
|
81
|
+
const pdf = flag(rest, "--pdf");
|
|
82
|
+
const pages = flag(rest, "--pages");
|
|
83
|
+
if (!pdf || !pages)
|
|
84
|
+
usage();
|
|
85
|
+
const { pageMin, pageMax } = parsePages(pages);
|
|
86
|
+
const includeTopics = parseTopics(flag(rest, "--topics"));
|
|
87
|
+
const title = flag(rest, "--title");
|
|
88
|
+
const common = {
|
|
89
|
+
pdfPath: pdf,
|
|
90
|
+
pageMin,
|
|
91
|
+
pageMax,
|
|
92
|
+
includeTopics,
|
|
93
|
+
title,
|
|
94
|
+
client: "cli"
|
|
95
|
+
};
|
|
96
|
+
if (cmd === "plan") {
|
|
97
|
+
const plan = await planCurriculum(common);
|
|
98
|
+
console.log(JSON.stringify({
|
|
99
|
+
note: "Free curriculum preview. Confirm, then run compile.",
|
|
100
|
+
plan
|
|
101
|
+
}, null, 2));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const result = await compileEpisode({
|
|
105
|
+
...common,
|
|
106
|
+
contentLanguage: flag(rest, "--lang") || "en",
|
|
107
|
+
characterId: flag(rest, "--character") || undefined,
|
|
108
|
+
plan: flag(rest, "--plan") ||
|
|
109
|
+
"free"
|
|
110
|
+
});
|
|
111
|
+
const jobId = String(result.job?.id || "");
|
|
112
|
+
console.log(JSON.stringify({ ...result, note: "1 episode credit charged." }, null, 2));
|
|
113
|
+
if (has(rest, "--wait") && jobId) {
|
|
114
|
+
const pollMs = Number(flag(rest, "--poll-ms") || 20000);
|
|
115
|
+
const finalJob = await waitForJob(jobId, Number.isFinite(pollMs) ? pollMs : 20000);
|
|
116
|
+
console.log(JSON.stringify({
|
|
117
|
+
job: finalJob,
|
|
118
|
+
studioUrl: `${getWebBase()}/app/projects/${jobId}`
|
|
119
|
+
}, null, 2));
|
|
120
|
+
if (String(finalJob.status) !== "succeeded")
|
|
121
|
+
process.exit(2);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
usage();
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const status = err.status;
|
|
129
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
130
|
+
if (status === 402) {
|
|
131
|
+
console.error("Insufficient credits (402). Upgrade plan or wait for monthly refresh.");
|
|
132
|
+
}
|
|
133
|
+
else if (status === 409) {
|
|
134
|
+
console.error("Another video is already generating (409). Wait or stop the running job.");
|
|
135
|
+
}
|
|
136
|
+
console.error(message);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,61 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* HiliFlow
|
|
3
|
+
* HiliFlow / docwhispr entry:
|
|
4
|
+
* - CLI: npx @docwhispr/mcp-server plan|compile|credits|status …
|
|
5
|
+
* - MCP: npx @docwhispr/mcp-server (stdio, via Cursor mcp.json)
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
* HILIFLOW_API_KEY
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Auth (first match wins):
|
|
8
|
+
* HILIFLOW_API_KEY env, or ~/.docwhispr/credentials.json from `configure`
|
|
9
|
+
* Optional:
|
|
10
|
+
* HILIFLOW_API_BASE / HILIFLOW_WEB_BASE
|
|
9
11
|
*/
|
|
10
12
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
13
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
14
|
import { z } from "zod";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
async function api(path, init = {}) {
|
|
31
|
-
const key = requireApiKey();
|
|
32
|
-
const headers = {
|
|
33
|
-
Authorization: `Bearer ${key}`,
|
|
34
|
-
"X-HiliFlow-Client": "mcp",
|
|
35
|
-
Accept: "application/json",
|
|
36
|
-
...init.headers
|
|
37
|
-
};
|
|
38
|
-
let body = init.body;
|
|
39
|
-
if (init.json !== undefined) {
|
|
40
|
-
headers["Content-Type"] = "application/json";
|
|
41
|
-
body = JSON.stringify(init.json);
|
|
42
|
-
}
|
|
43
|
-
const res = await fetch(`${API_BASE}${path}`, { ...init, headers, body });
|
|
44
|
-
const text = await res.text();
|
|
45
|
-
let data = null;
|
|
46
|
-
try {
|
|
47
|
-
data = text ? JSON.parse(text) : null;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
data = { raw: text };
|
|
51
|
-
}
|
|
52
|
-
if (!res.ok) {
|
|
53
|
-
const detail = typeof data === "object" && data && "detail" in data
|
|
54
|
-
? String(data.detail)
|
|
55
|
-
: text || res.statusText;
|
|
56
|
-
throw new Error(`HiliFlow API ${res.status}: ${detail}`);
|
|
57
|
-
}
|
|
58
|
-
return data;
|
|
15
|
+
import { api, buildTeachingScope, compileEpisode, fileNameOf, fileToBase64, getWebBase, jobStatus, planCurriculum, sha256HexOfFile } from "./lib.js";
|
|
16
|
+
const CLI_COMMANDS = new Set([
|
|
17
|
+
"configure",
|
|
18
|
+
"plan",
|
|
19
|
+
"compile",
|
|
20
|
+
"credits",
|
|
21
|
+
"status",
|
|
22
|
+
"help",
|
|
23
|
+
"-h",
|
|
24
|
+
"--help"
|
|
25
|
+
]);
|
|
26
|
+
const maybeCmd = process.argv[2];
|
|
27
|
+
if (maybeCmd && CLI_COMMANDS.has(maybeCmd)) {
|
|
28
|
+
const { run } = await import("./cli.js");
|
|
29
|
+
await run(process.argv.slice(2));
|
|
30
|
+
process.exit(0);
|
|
59
31
|
}
|
|
60
32
|
function textResult(payload) {
|
|
61
33
|
return {
|
|
@@ -67,29 +39,9 @@ function textResult(payload) {
|
|
|
67
39
|
]
|
|
68
40
|
};
|
|
69
41
|
}
|
|
70
|
-
function fileToBase64(filePath) {
|
|
71
|
-
const abs = resolve(filePath);
|
|
72
|
-
return readFileSync(abs).toString("base64");
|
|
73
|
-
}
|
|
74
|
-
async function sha256HexOfFile(filePath) {
|
|
75
|
-
const { createHash } = await import("node:crypto");
|
|
76
|
-
const buf = readFileSync(resolve(filePath));
|
|
77
|
-
return createHash("sha256").update(buf).digest("hex");
|
|
78
|
-
}
|
|
79
|
-
const teachingScopeSchema = z
|
|
80
|
-
.object({
|
|
81
|
-
include_topics: z.array(z.string()).optional(),
|
|
82
|
-
exclude_topics: z.array(z.string()).optional(),
|
|
83
|
-
background_policy: z
|
|
84
|
-
.enum(["none", "necessary_only", "all_in_section"])
|
|
85
|
-
.optional(),
|
|
86
|
-
page_numbering: z.enum(["auto", "pdf", "book"]).optional(),
|
|
87
|
-
user_prompt: z.string().optional()
|
|
88
|
-
})
|
|
89
|
-
.optional();
|
|
90
42
|
const server = new McpServer({
|
|
91
43
|
name: "hiliflow",
|
|
92
|
-
version: "0.1.
|
|
44
|
+
version: "0.1.1"
|
|
93
45
|
});
|
|
94
46
|
server.tool("hiliflow_credits", {
|
|
95
47
|
plan: z.enum(["free", "pro", "pro_plus"]).optional()
|
|
@@ -117,31 +69,16 @@ server.tool("hiliflow_plan_curriculum", {
|
|
|
117
69
|
maxPagesPerJob: z.number().int().min(1).max(20).optional().default(10),
|
|
118
70
|
title: z.string().optional()
|
|
119
71
|
}, async (args) => {
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
131
|
-
const plan = await api("/api/series/plan", {
|
|
132
|
-
method: "POST",
|
|
133
|
-
json: {
|
|
134
|
-
fileName: resolve(args.pdfPath).split(/[/\\]/).pop(),
|
|
135
|
-
pageMin: args.pageMin,
|
|
136
|
-
pageMax: args.pageMax,
|
|
137
|
-
contentHash,
|
|
138
|
-
contentBase64,
|
|
139
|
-
sourceType: "pdf",
|
|
140
|
-
title: args.title,
|
|
141
|
-
teachingScope,
|
|
142
|
-
maxPagesPerJob: args.maxPagesPerJob ?? 10,
|
|
143
|
-
extractDocName: true
|
|
144
|
-
}
|
|
72
|
+
void args.pageNumbering; // always auto in teachingScope
|
|
73
|
+
const plan = await planCurriculum({
|
|
74
|
+
pdfPath: args.pdfPath,
|
|
75
|
+
pageMin: args.pageMin,
|
|
76
|
+
pageMax: args.pageMax,
|
|
77
|
+
includeTopics: args.includeTopics,
|
|
78
|
+
backgroundPolicy: args.backgroundPolicy,
|
|
79
|
+
userPrompt: args.userPrompt,
|
|
80
|
+
maxPagesPerJob: args.maxPagesPerJob,
|
|
81
|
+
title: args.title
|
|
145
82
|
});
|
|
146
83
|
return textResult({
|
|
147
84
|
note: "Free curriculum preview. Confirm with the user, then call hiliflow_compile_series (or hiliflow_compile_episode for a single job).",
|
|
@@ -161,37 +98,27 @@ server.tool("hiliflow_compile_episode", {
|
|
|
161
98
|
userPrompt: z.string().optional(),
|
|
162
99
|
title: z.string().optional(),
|
|
163
100
|
contentLanguage: z.string().optional().default("en"),
|
|
101
|
+
characterId: z
|
|
102
|
+
.string()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe('e.g. "chr_pixel_voxel" for Voxel teacher'),
|
|
164
105
|
plan: z.enum(["free", "pro", "pro_plus"]).optional().default("free")
|
|
165
106
|
}, async (args) => {
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
pageMin: args.pageMin,
|
|
179
|
-
pageMax: args.pageMax,
|
|
180
|
-
contentHash,
|
|
181
|
-
contentBase64,
|
|
182
|
-
sourceType: "pdf",
|
|
183
|
-
title: args.title,
|
|
184
|
-
teachingScope,
|
|
185
|
-
contentLanguage: args.contentLanguage ?? "en",
|
|
186
|
-
plan: args.plan ?? "free",
|
|
187
|
-
extractDocName: true,
|
|
188
|
-
highlight: true
|
|
189
|
-
}
|
|
107
|
+
void args.pageNumbering;
|
|
108
|
+
const result = await compileEpisode({
|
|
109
|
+
pdfPath: args.pdfPath,
|
|
110
|
+
pageMin: args.pageMin,
|
|
111
|
+
pageMax: args.pageMax,
|
|
112
|
+
includeTopics: args.includeTopics,
|
|
113
|
+
backgroundPolicy: args.backgroundPolicy,
|
|
114
|
+
userPrompt: args.userPrompt,
|
|
115
|
+
title: args.title,
|
|
116
|
+
contentLanguage: args.contentLanguage,
|
|
117
|
+
characterId: args.characterId,
|
|
118
|
+
plan: args.plan
|
|
190
119
|
});
|
|
191
|
-
const jobId = String(job.id || "");
|
|
192
120
|
return textResult({
|
|
193
|
-
|
|
194
|
-
studioUrl: jobId ? `${WEB_BASE}/app/projects/${jobId}` : null,
|
|
121
|
+
...result,
|
|
195
122
|
note: "1 episode credit charged. Poll hiliflow_job_status until status=succeeded."
|
|
196
123
|
});
|
|
197
124
|
});
|
|
@@ -209,20 +136,20 @@ server.tool("hiliflow_compile_series", {
|
|
|
209
136
|
maxPagesPerJob: z.number().int().min(1).max(20).optional().default(10),
|
|
210
137
|
title: z.string().optional(),
|
|
211
138
|
contentLanguage: z.string().optional().default("en"),
|
|
139
|
+
characterId: z
|
|
140
|
+
.string()
|
|
141
|
+
.optional()
|
|
142
|
+
.describe('e.g. "chr_pixel_voxel" for Voxel teacher'),
|
|
212
143
|
plan: z.enum(["free", "pro", "pro_plus"]).optional().default("free")
|
|
213
144
|
}, async (args) => {
|
|
145
|
+
void args.pageNumbering;
|
|
214
146
|
const contentHash = await sha256HexOfFile(args.pdfPath);
|
|
215
147
|
const contentBase64 = fileToBase64(args.pdfPath);
|
|
216
|
-
const teachingScope =
|
|
217
|
-
include_topics: args.includeTopics ?? [],
|
|
218
|
-
background_policy: args.backgroundPolicy ?? "necessary_only",
|
|
219
|
-
page_numbering: args.pageNumbering ?? "auto",
|
|
220
|
-
user_prompt: args.userPrompt || ""
|
|
221
|
-
};
|
|
148
|
+
const teachingScope = buildTeachingScope(args);
|
|
222
149
|
const result = await api("/api/series/create", {
|
|
223
150
|
method: "POST",
|
|
224
151
|
json: {
|
|
225
|
-
fileName:
|
|
152
|
+
fileName: fileNameOf(args.pdfPath),
|
|
226
153
|
pageMin: args.pageMin,
|
|
227
154
|
pageMax: args.pageMax,
|
|
228
155
|
contentHash,
|
|
@@ -232,6 +159,7 @@ server.tool("hiliflow_compile_series", {
|
|
|
232
159
|
teachingScope,
|
|
233
160
|
maxPagesPerJob: args.maxPagesPerJob ?? 10,
|
|
234
161
|
contentLanguage: args.contentLanguage ?? "en",
|
|
162
|
+
...(args.characterId ? { characterId: args.characterId } : {}),
|
|
235
163
|
plan: args.plan ?? "free",
|
|
236
164
|
extractDocName: true,
|
|
237
165
|
highlight: true
|
|
@@ -242,27 +170,19 @@ server.tool("hiliflow_compile_series", {
|
|
|
242
170
|
studioUrls: (result.jobs || []).map((j) => ({
|
|
243
171
|
jobId: j.id,
|
|
244
172
|
title: j.title,
|
|
245
|
-
studioUrl: `${
|
|
173
|
+
studioUrl: `${getWebBase()}/app/projects/${j.id}`
|
|
246
174
|
})),
|
|
247
175
|
note: `${result.totalEpisodes} episode credit(s) charged.`
|
|
248
176
|
});
|
|
249
177
|
});
|
|
250
178
|
server.tool("hiliflow_job_status", {
|
|
251
179
|
jobId: z.string()
|
|
252
|
-
}, async ({ jobId }) =>
|
|
253
|
-
const job = await api(`/api/jobs/${jobId}`);
|
|
254
|
-
return textResult({
|
|
255
|
-
...job,
|
|
256
|
-
studioUrl: `${WEB_BASE}/app/projects/${jobId}`
|
|
257
|
-
});
|
|
258
|
-
});
|
|
180
|
+
}, async ({ jobId }) => textResult(await jobStatus(jobId)));
|
|
259
181
|
server.tool("hiliflow_open_studio", {
|
|
260
182
|
jobId: z.string()
|
|
261
183
|
}, async ({ jobId }) => {
|
|
262
184
|
const data = await api(`/api/mcp/studio/${jobId}`);
|
|
263
185
|
return textResult(data);
|
|
264
186
|
});
|
|
265
|
-
// silence unused schema warning in some bundlers
|
|
266
|
-
void teachingScopeSchema;
|
|
267
187
|
const transport = new StdioServerTransport();
|
|
268
188
|
await server.connect(transport);
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type StoredCredentials = {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
apiBase?: string;
|
|
4
|
+
webBase?: string;
|
|
5
|
+
updatedAt?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function credentialsDir(): string;
|
|
8
|
+
export declare function credentialsPath(): string;
|
|
9
|
+
export declare function readCredentials(): StoredCredentials;
|
|
10
|
+
/** Persist API key for CLI/MCP (env still wins when set). */
|
|
11
|
+
export declare function saveCredentials(input: {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
apiBase?: string;
|
|
14
|
+
webBase?: string;
|
|
15
|
+
}): string;
|
|
16
|
+
/** Ensure Cursor user MCP config points at docwhispr without embedding the key. */
|
|
17
|
+
export declare function ensureCursorMcpConfig(): string;
|
|
18
|
+
export declare function getApiBase(): string;
|
|
19
|
+
export declare function getWebBase(): string;
|
|
20
|
+
/** @deprecated use getApiBase() — kept for call sites that import API_BASE */
|
|
21
|
+
export declare const API_BASE: string;
|
|
22
|
+
/** @deprecated use getWebBase() — kept for call sites that import WEB_BASE */
|
|
23
|
+
export declare const WEB_BASE: string;
|
|
24
|
+
export declare function requireApiKey(): string;
|
|
25
|
+
export declare function api<T>(path: string, init?: RequestInit & {
|
|
26
|
+
json?: unknown;
|
|
27
|
+
}, client?: string): Promise<T>;
|
|
28
|
+
export declare function fileToBase64(filePath: string): string;
|
|
29
|
+
export declare function sha256HexOfFile(filePath: string): Promise<string>;
|
|
30
|
+
export declare function fileNameOf(filePath: string): string;
|
|
31
|
+
export type TeachingScopeInput = {
|
|
32
|
+
includeTopics?: string[];
|
|
33
|
+
backgroundPolicy?: "none" | "necessary_only" | "all_in_section";
|
|
34
|
+
userPrompt?: string;
|
|
35
|
+
};
|
|
36
|
+
export declare function buildTeachingScope(input: TeachingScopeInput): {
|
|
37
|
+
include_topics: string[];
|
|
38
|
+
background_policy: "none" | "necessary_only" | "all_in_section";
|
|
39
|
+
page_numbering: "auto";
|
|
40
|
+
user_prompt: string;
|
|
41
|
+
};
|
|
42
|
+
export type PlanArgs = {
|
|
43
|
+
pdfPath: string;
|
|
44
|
+
pageMin: number;
|
|
45
|
+
pageMax: number;
|
|
46
|
+
includeTopics?: string[];
|
|
47
|
+
backgroundPolicy?: "none" | "necessary_only" | "all_in_section";
|
|
48
|
+
userPrompt?: string;
|
|
49
|
+
maxPagesPerJob?: number;
|
|
50
|
+
title?: string;
|
|
51
|
+
client?: string;
|
|
52
|
+
};
|
|
53
|
+
export declare function planCurriculum(args: PlanArgs): Promise<unknown>;
|
|
54
|
+
export type CompileArgs = PlanArgs & {
|
|
55
|
+
contentLanguage?: string;
|
|
56
|
+
characterId?: string;
|
|
57
|
+
plan?: "free" | "pro" | "pro_plus";
|
|
58
|
+
};
|
|
59
|
+
export declare function compileEpisode(args: CompileArgs): Promise<{
|
|
60
|
+
job: Record<string, unknown>;
|
|
61
|
+
studioUrl: string | null;
|
|
62
|
+
}>;
|
|
63
|
+
export declare function jobStatus(jobId: string, client?: string): Promise<Record<string, unknown> & {
|
|
64
|
+
studioUrl: string;
|
|
65
|
+
}>;
|
|
66
|
+
export declare function parsePages(raw: string): {
|
|
67
|
+
pageMin: number;
|
|
68
|
+
pageMax: number;
|
|
69
|
+
};
|
|
70
|
+
export declare function parseTopics(raw: string | undefined): string[] | undefined;
|
package/dist/lib.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared HiliFlow / docwhispr API helpers for MCP tools and CLI.
|
|
3
|
+
*/
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
export function credentialsDir() {
|
|
9
|
+
return join(homedir(), ".docwhispr");
|
|
10
|
+
}
|
|
11
|
+
export function credentialsPath() {
|
|
12
|
+
return join(credentialsDir(), "credentials.json");
|
|
13
|
+
}
|
|
14
|
+
export function readCredentials() {
|
|
15
|
+
try {
|
|
16
|
+
const raw = readFileSync(credentialsPath(), "utf8");
|
|
17
|
+
const data = JSON.parse(raw);
|
|
18
|
+
return data && typeof data === "object" ? data : {};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Persist API key for CLI/MCP (env still wins when set). */
|
|
25
|
+
export function saveCredentials(input) {
|
|
26
|
+
const key = input.apiKey.trim();
|
|
27
|
+
if (!key.startsWith("hl_live_")) {
|
|
28
|
+
throw new Error('API key must start with "hl_live_"');
|
|
29
|
+
}
|
|
30
|
+
const prev = readCredentials();
|
|
31
|
+
const dir = credentialsDir();
|
|
32
|
+
mkdirSync(dir, { recursive: true });
|
|
33
|
+
const path = credentialsPath();
|
|
34
|
+
const payload = {
|
|
35
|
+
apiKey: key,
|
|
36
|
+
apiBase: (input.apiBase ||
|
|
37
|
+
prev.apiBase ||
|
|
38
|
+
"https://docwhispr.fly.dev").replace(/\/$/, ""),
|
|
39
|
+
webBase: (input.webBase ||
|
|
40
|
+
prev.webBase ||
|
|
41
|
+
"https://docwhispr.com").replace(/\/$/, ""),
|
|
42
|
+
updatedAt: new Date().toISOString()
|
|
43
|
+
};
|
|
44
|
+
writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, {
|
|
45
|
+
encoding: "utf8",
|
|
46
|
+
mode: 0o600
|
|
47
|
+
});
|
|
48
|
+
return path;
|
|
49
|
+
}
|
|
50
|
+
/** Ensure Cursor user MCP config points at docwhispr without embedding the key. */
|
|
51
|
+
export function ensureCursorMcpConfig() {
|
|
52
|
+
const cursorDir = join(homedir(), ".cursor");
|
|
53
|
+
mkdirSync(cursorDir, { recursive: true });
|
|
54
|
+
const path = join(cursorDir, "mcp.json");
|
|
55
|
+
let root = { mcpServers: {} };
|
|
56
|
+
if (existsSync(path)) {
|
|
57
|
+
try {
|
|
58
|
+
root = JSON.parse(readFileSync(path, "utf8"));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
root = { mcpServers: {} };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!root.mcpServers || typeof root.mcpServers !== "object") {
|
|
65
|
+
root.mcpServers = {};
|
|
66
|
+
}
|
|
67
|
+
root.mcpServers.docwhispr = {
|
|
68
|
+
command: "npx",
|
|
69
|
+
args: ["-y", "@docwhispr/mcp-server"],
|
|
70
|
+
env: {
|
|
71
|
+
HILIFLOW_API_BASE: getApiBase(),
|
|
72
|
+
HILIFLOW_WEB_BASE: getWebBase()
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
writeFileSync(path, `${JSON.stringify(root, null, 2)}\n`, "utf8");
|
|
76
|
+
return path;
|
|
77
|
+
}
|
|
78
|
+
export function getApiBase() {
|
|
79
|
+
return (process.env.HILIFLOW_API_BASE ||
|
|
80
|
+
process.env.DOCWHISPR_API_BASE ||
|
|
81
|
+
process.env.NEXT_PUBLIC_VIDEO_WORKER_BASE_URL ||
|
|
82
|
+
readCredentials().apiBase ||
|
|
83
|
+
"https://docwhispr.fly.dev").replace(/\/$/, "");
|
|
84
|
+
}
|
|
85
|
+
export function getWebBase() {
|
|
86
|
+
return (process.env.HILIFLOW_WEB_BASE ||
|
|
87
|
+
process.env.DOCWHISPR_WEB_BASE ||
|
|
88
|
+
process.env.NEXT_PUBLIC_APP_URL ||
|
|
89
|
+
readCredentials().webBase ||
|
|
90
|
+
"https://docwhispr.com").replace(/\/$/, "");
|
|
91
|
+
}
|
|
92
|
+
/** @deprecated use getApiBase() — kept for call sites that import API_BASE */
|
|
93
|
+
export const API_BASE = getApiBase();
|
|
94
|
+
/** @deprecated use getWebBase() — kept for call sites that import WEB_BASE */
|
|
95
|
+
export const WEB_BASE = getWebBase();
|
|
96
|
+
export function requireApiKey() {
|
|
97
|
+
const fromEnv = (process.env.HILIFLOW_API_KEY ||
|
|
98
|
+
process.env.DOCWHISPR_API_KEY ||
|
|
99
|
+
"").trim();
|
|
100
|
+
if (fromEnv)
|
|
101
|
+
return fromEnv;
|
|
102
|
+
const fromFile = (readCredentials().apiKey || "").trim();
|
|
103
|
+
if (fromFile)
|
|
104
|
+
return fromFile;
|
|
105
|
+
throw new Error(`No API key found. Run: npx @docwhispr/mcp-server configure --key hl_live_…\n` +
|
|
106
|
+
`Or set HILIFLOW_API_KEY / write ${credentialsPath()}`);
|
|
107
|
+
}
|
|
108
|
+
export async function api(path, init = {}, client = "mcp") {
|
|
109
|
+
const key = requireApiKey();
|
|
110
|
+
const headers = {
|
|
111
|
+
Authorization: `Bearer ${key}`,
|
|
112
|
+
"X-HiliFlow-Client": client,
|
|
113
|
+
Accept: "application/json",
|
|
114
|
+
...init.headers
|
|
115
|
+
};
|
|
116
|
+
let body = init.body;
|
|
117
|
+
if (init.json !== undefined) {
|
|
118
|
+
headers["Content-Type"] = "application/json";
|
|
119
|
+
body = JSON.stringify(init.json);
|
|
120
|
+
}
|
|
121
|
+
const res = await fetch(`${getApiBase()}${path}`, { ...init, headers, body });
|
|
122
|
+
const text = await res.text();
|
|
123
|
+
let data = null;
|
|
124
|
+
try {
|
|
125
|
+
data = text ? JSON.parse(text) : null;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
data = { raw: text };
|
|
129
|
+
}
|
|
130
|
+
if (!res.ok) {
|
|
131
|
+
const detail = typeof data === "object" && data && "detail" in data
|
|
132
|
+
? String(data.detail)
|
|
133
|
+
: text || res.statusText;
|
|
134
|
+
const err = new Error(`HiliFlow API ${res.status}: ${detail}`);
|
|
135
|
+
err.status = res.status;
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
return data;
|
|
139
|
+
}
|
|
140
|
+
export function fileToBase64(filePath) {
|
|
141
|
+
return readFileSync(resolve(filePath)).toString("base64");
|
|
142
|
+
}
|
|
143
|
+
export async function sha256HexOfFile(filePath) {
|
|
144
|
+
const buf = readFileSync(resolve(filePath));
|
|
145
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
146
|
+
}
|
|
147
|
+
export function fileNameOf(filePath) {
|
|
148
|
+
return resolve(filePath).split(/[/\\]/).pop() || "document.pdf";
|
|
149
|
+
}
|
|
150
|
+
export function buildTeachingScope(input) {
|
|
151
|
+
const topics = input.includeTopics ?? [];
|
|
152
|
+
return {
|
|
153
|
+
include_topics: topics,
|
|
154
|
+
background_policy: input.backgroundPolicy ?? "necessary_only",
|
|
155
|
+
page_numbering: "auto",
|
|
156
|
+
user_prompt: input.userPrompt ||
|
|
157
|
+
(topics.length ? `Focus on: ${topics.join(", ")}` : "")
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
export async function planCurriculum(args) {
|
|
161
|
+
const contentHash = await sha256HexOfFile(args.pdfPath);
|
|
162
|
+
const contentBase64 = fileToBase64(args.pdfPath);
|
|
163
|
+
const teachingScope = buildTeachingScope(args);
|
|
164
|
+
return api("/api/series/plan", {
|
|
165
|
+
method: "POST",
|
|
166
|
+
json: {
|
|
167
|
+
fileName: fileNameOf(args.pdfPath),
|
|
168
|
+
pageMin: args.pageMin,
|
|
169
|
+
pageMax: args.pageMax,
|
|
170
|
+
contentHash,
|
|
171
|
+
contentBase64,
|
|
172
|
+
sourceType: "pdf",
|
|
173
|
+
title: args.title,
|
|
174
|
+
teachingScope,
|
|
175
|
+
maxPagesPerJob: args.maxPagesPerJob ?? 10,
|
|
176
|
+
extractDocName: true
|
|
177
|
+
}
|
|
178
|
+
}, args.client ?? "mcp");
|
|
179
|
+
}
|
|
180
|
+
export async function compileEpisode(args) {
|
|
181
|
+
const contentHash = await sha256HexOfFile(args.pdfPath);
|
|
182
|
+
const contentBase64 = fileToBase64(args.pdfPath);
|
|
183
|
+
const teachingScope = buildTeachingScope(args);
|
|
184
|
+
const job = await api("/api/jobs", {
|
|
185
|
+
method: "POST",
|
|
186
|
+
json: {
|
|
187
|
+
fileName: fileNameOf(args.pdfPath),
|
|
188
|
+
pageMin: args.pageMin,
|
|
189
|
+
pageMax: args.pageMax,
|
|
190
|
+
contentHash,
|
|
191
|
+
contentBase64,
|
|
192
|
+
sourceType: "pdf",
|
|
193
|
+
title: args.title,
|
|
194
|
+
teachingScope,
|
|
195
|
+
contentLanguage: args.contentLanguage ?? "en",
|
|
196
|
+
...(args.characterId ? { characterId: args.characterId } : {}),
|
|
197
|
+
plan: args.plan ?? "free",
|
|
198
|
+
extractDocName: true,
|
|
199
|
+
highlight: true
|
|
200
|
+
}
|
|
201
|
+
}, args.client ?? "mcp");
|
|
202
|
+
const jobId = String(job.id || "");
|
|
203
|
+
return {
|
|
204
|
+
job,
|
|
205
|
+
studioUrl: jobId ? `${getWebBase()}/app/projects/${jobId}` : null
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
export async function jobStatus(jobId, client = "mcp") {
|
|
209
|
+
const job = await api(`/api/jobs/${jobId}`, {}, client);
|
|
210
|
+
return {
|
|
211
|
+
...job,
|
|
212
|
+
studioUrl: `${getWebBase()}/app/projects/${jobId}`
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export function parsePages(raw) {
|
|
216
|
+
const m = String(raw || "")
|
|
217
|
+
.trim()
|
|
218
|
+
.match(/^(\d+)\s*[-–—:]\s*(\d+)$/);
|
|
219
|
+
if (!m) {
|
|
220
|
+
throw new Error(`Invalid --pages value "${raw}". Use e.g. 602-604`);
|
|
221
|
+
}
|
|
222
|
+
const pageMin = Number(m[1]);
|
|
223
|
+
const pageMax = Number(m[2]);
|
|
224
|
+
if (!(pageMin >= 1 && pageMax >= pageMin)) {
|
|
225
|
+
throw new Error(`Invalid page range ${pageMin}-${pageMax}`);
|
|
226
|
+
}
|
|
227
|
+
return { pageMin, pageMax };
|
|
228
|
+
}
|
|
229
|
+
export function parseTopics(raw) {
|
|
230
|
+
if (!raw?.trim())
|
|
231
|
+
return undefined;
|
|
232
|
+
const parts = raw
|
|
233
|
+
.split(/[,;,;\n]+/)
|
|
234
|
+
.map((s) => s.trim())
|
|
235
|
+
.filter(Boolean);
|
|
236
|
+
return parts.length ? parts : undefined;
|
|
237
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docwhispr/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "docwhispr MCP server — plan curriculum, compile whiteboard lesson videos, open interactive studio",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "docwhispr MCP server + CLI — plan curriculum, compile whiteboard lesson videos, open interactive studio",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
+
"mcp-server": "dist/index.js",
|
|
8
|
+
"docwhispr": "dist/index.js",
|
|
7
9
|
"docwhispr-mcp": "dist/index.js",
|
|
8
10
|
"hiliflow-mcp": "dist/index.js"
|
|
9
11
|
},
|