@docwhispr/mcp-server 0.0.0-stage → 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 CHANGED
@@ -1,3 +1,77 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @docwhispr/mcp-server
2
+
3
+ Thin client for **docwhispr** cloud curriculum compiler (HiliFlow).
4
+
5
+ ## One-time setup (paste from Settings)
6
+
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).
14
+
15
+ ## CLI
16
+
17
+ ```bash
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
30
+ ```
31
+
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`.
40
+
41
+ `402` = out of credits · `409` = another job already generating.
42
+
43
+ ## MCP stdio
44
+
45
+ After `configure --cursor-mcp`, or manually:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "docwhispr": {
51
+ "command": "npx",
52
+ "args": ["-y", "@docwhispr/mcp-server"],
53
+ "env": {
54
+ "HILIFLOW_API_BASE": "https://docwhispr.fly.dev",
55
+ "HILIFLOW_WEB_BASE": "https://docwhispr.com"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
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 |
70
+
71
+ ## Monthly credits
72
+
73
+ | Plan | Episodes / month |
74
+ | --- | ---: |
75
+ | free | 5 |
76
+ | pro | 50 |
77
+ | pro_plus | 100 |
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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env node
2
+ /**
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)
6
+ *
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
11
+ */
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { z } from "zod";
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);
31
+ }
32
+ function textResult(payload) {
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: typeof payload === "string" ? payload : JSON.stringify(payload, null, 2)
38
+ }
39
+ ]
40
+ };
41
+ }
42
+ const server = new McpServer({
43
+ name: "hiliflow",
44
+ version: "0.1.1"
45
+ });
46
+ server.tool("hiliflow_credits", {
47
+ plan: z.enum(["free", "pro", "pro_plus"]).optional()
48
+ }, async ({ plan }) => {
49
+ const q = plan ? `?plan=${encodeURIComponent(plan)}` : "";
50
+ const data = await api(`/api/mcp/credits${q}`);
51
+ return textResult(data);
52
+ });
53
+ server.tool("hiliflow_plan_curriculum", {
54
+ pdfPath: z
55
+ .string()
56
+ .describe("Absolute path to a local PDF on the machine running the MCP server"),
57
+ pageMin: z.number().int().min(1),
58
+ pageMax: z.number().int().min(1),
59
+ includeTopics: z
60
+ .array(z.string())
61
+ .optional()
62
+ .describe('e.g. ["EXTEST","INTEST"]'),
63
+ backgroundPolicy: z
64
+ .enum(["none", "necessary_only", "all_in_section"])
65
+ .optional()
66
+ .default("necessary_only"),
67
+ pageNumbering: z.enum(["auto", "pdf", "book"]).optional().default("auto"),
68
+ userPrompt: z.string().optional(),
69
+ maxPagesPerJob: z.number().int().min(1).max(20).optional().default(10),
70
+ title: z.string().optional()
71
+ }, async (args) => {
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
82
+ });
83
+ return textResult({
84
+ note: "Free curriculum preview. Confirm with the user, then call hiliflow_compile_series (or hiliflow_compile_episode for a single job).",
85
+ plan
86
+ });
87
+ });
88
+ server.tool("hiliflow_compile_episode", {
89
+ pdfPath: z.string(),
90
+ pageMin: z.number().int().min(1),
91
+ pageMax: z.number().int().min(1),
92
+ includeTopics: z.array(z.string()).optional(),
93
+ backgroundPolicy: z
94
+ .enum(["none", "necessary_only", "all_in_section"])
95
+ .optional()
96
+ .default("necessary_only"),
97
+ pageNumbering: z.enum(["auto", "pdf", "book"]).optional().default("auto"),
98
+ userPrompt: z.string().optional(),
99
+ title: z.string().optional(),
100
+ contentLanguage: z.string().optional().default("en"),
101
+ characterId: z
102
+ .string()
103
+ .optional()
104
+ .describe('e.g. "chr_pixel_voxel" for Voxel teacher'),
105
+ plan: z.enum(["free", "pro", "pro_plus"]).optional().default("free")
106
+ }, async (args) => {
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
119
+ });
120
+ return textResult({
121
+ ...result,
122
+ note: "1 episode credit charged. Poll hiliflow_job_status until status=succeeded."
123
+ });
124
+ });
125
+ server.tool("hiliflow_compile_series", {
126
+ pdfPath: z.string(),
127
+ pageMin: z.number().int().min(1),
128
+ pageMax: z.number().int().min(1),
129
+ includeTopics: z.array(z.string()).optional(),
130
+ backgroundPolicy: z
131
+ .enum(["none", "necessary_only", "all_in_section"])
132
+ .optional()
133
+ .default("necessary_only"),
134
+ pageNumbering: z.enum(["auto", "pdf", "book"]).optional().default("auto"),
135
+ userPrompt: z.string().optional(),
136
+ maxPagesPerJob: z.number().int().min(1).max(20).optional().default(10),
137
+ title: z.string().optional(),
138
+ contentLanguage: z.string().optional().default("en"),
139
+ characterId: z
140
+ .string()
141
+ .optional()
142
+ .describe('e.g. "chr_pixel_voxel" for Voxel teacher'),
143
+ plan: z.enum(["free", "pro", "pro_plus"]).optional().default("free")
144
+ }, async (args) => {
145
+ void args.pageNumbering;
146
+ const contentHash = await sha256HexOfFile(args.pdfPath);
147
+ const contentBase64 = fileToBase64(args.pdfPath);
148
+ const teachingScope = buildTeachingScope(args);
149
+ const result = await api("/api/series/create", {
150
+ method: "POST",
151
+ json: {
152
+ fileName: fileNameOf(args.pdfPath),
153
+ pageMin: args.pageMin,
154
+ pageMax: args.pageMax,
155
+ contentHash,
156
+ contentBase64,
157
+ sourceType: "pdf",
158
+ title: args.title,
159
+ teachingScope,
160
+ maxPagesPerJob: args.maxPagesPerJob ?? 10,
161
+ contentLanguage: args.contentLanguage ?? "en",
162
+ ...(args.characterId ? { characterId: args.characterId } : {}),
163
+ plan: args.plan ?? "free",
164
+ extractDocName: true,
165
+ highlight: true
166
+ }
167
+ });
168
+ return textResult({
169
+ ...result,
170
+ studioUrls: (result.jobs || []).map((j) => ({
171
+ jobId: j.id,
172
+ title: j.title,
173
+ studioUrl: `${getWebBase()}/app/projects/${j.id}`
174
+ })),
175
+ note: `${result.totalEpisodes} episode credit(s) charged.`
176
+ });
177
+ });
178
+ server.tool("hiliflow_job_status", {
179
+ jobId: z.string()
180
+ }, async ({ jobId }) => textResult(await jobStatus(jobId)));
181
+ server.tool("hiliflow_open_studio", {
182
+ jobId: z.string()
183
+ }, async ({ jobId }) => {
184
+ const data = await api(`/api/mcp/studio/${jobId}`);
185
+ return textResult(data);
186
+ });
187
+ const transport = new StdioServerTransport();
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,6 +1,57 @@
1
- {
2
- "name": "@docwhispr/mcp-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@docwhispr/mcp-server",
3
+ "version": "0.1.3",
4
+ "description": "docwhispr MCP server + CLI — plan curriculum, compile whiteboard lesson videos, open interactive studio",
5
+ "type": "module",
6
+ "bin": {
7
+ "mcp-server": "dist/index.js",
8
+ "docwhispr": "dist/index.js",
9
+ "docwhispr-mcp": "dist/index.js",
10
+ "hiliflow-mcp": "dist/index.js"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "prepublishOnly": "npm run build",
20
+ "start": "node dist/index.js",
21
+ "dev": "tsx src/index.ts",
22
+ "typecheck": "tsc -p tsconfig.json --noEmit"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "keywords": [
28
+ "mcp",
29
+ "docwhispr",
30
+ "hiliflow",
31
+ "curriculum",
32
+ "whiteboard",
33
+ "pdf"
34
+ ],
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/zhenjun-avatar/docwhispr.git",
39
+ "directory": "packages/mcp-server"
40
+ },
41
+ "homepage": "https://docwhispr.com",
42
+ "bugs": {
43
+ "url": "https://github.com/zhenjun-avatar/docwhispr/issues"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@modelcontextprotocol/sdk": "^1.11.2",
50
+ "zod": "^3.24.2"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^22.13.10",
54
+ "tsx": "^4.19.3",
55
+ "typescript": "^5.8.2"
56
+ }
57
+ }