@docwhispr/mcp-server 0.0.0-stage → 0.1.0

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,75 @@
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 MCP client for **docwhispr** cloud curriculum compiler (HiliFlow).
4
+
5
+ - Free: `hiliflow_plan_curriculum` (topic scope + multi-episode blueprint)
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
8
+
9
+ ## Setup
10
+
11
+ 1. Sign in at [docwhispr.com](https://docwhispr.com), create an API key:
12
+
13
+ ```bash
14
+ curl -X POST "$HILIFLOW_API_BASE/api/mcp/keys" \
15
+ -H "Authorization: Bearer <session_jwt>" \
16
+ -H "Content-Type: application/json" \
17
+ -d '{"name":"Cursor MCP","plan":"free"}'
18
+ ```
19
+
20
+ Save the returned `apiKey` (`hl_live_…`) once.
21
+
22
+ 2. Cursor `mcp.json` (or Claude Desktop MCP config):
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "docwhispr": {
28
+ "command": "npx",
29
+ "args": ["-y", "@docwhispr/mcp-server"],
30
+ "env": {
31
+ "HILIFLOW_API_KEY": "hl_live_…",
32
+ "HILIFLOW_API_BASE": "https://docwhispr.fly.dev",
33
+ "HILIFLOW_WEB_BASE": "https://docwhispr.com"
34
+ }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ Local monorepo:
41
+
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "docwhispr": {
46
+ "command": "npx",
47
+ "args": ["tsx", "packages/mcp-server/src/index.ts"],
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 |
68
+
69
+ ## Monthly credits (same pool as web Generate)
70
+
71
+ | Plan | Episodes / month |
72
+ | --- | ---: |
73
+ | free | 5 |
74
+ | pro | 50 |
75
+ | pro_plus | 100 |
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * HiliFlow MCP server (stdio).
4
+ *
5
+ * Env:
6
+ * HILIFLOW_API_KEY — required (hl_live_…)
7
+ * HILIFLOW_API_BASE — optional, default https://docwhispr.fly.dev
8
+ * HILIFLOW_WEB_BASE — optional studio host, default https://docwhispr.com
9
+ */
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { z } from "zod";
13
+ import { readFileSync } from "node:fs";
14
+ import { resolve } from "node:path";
15
+ const API_BASE = (process.env.HILIFLOW_API_BASE ||
16
+ process.env.DOCWHISPR_API_BASE ||
17
+ process.env.NEXT_PUBLIC_VIDEO_WORKER_BASE_URL ||
18
+ "https://docwhispr.fly.dev").replace(/\/$/, "");
19
+ const WEB_BASE = (process.env.HILIFLOW_WEB_BASE ||
20
+ process.env.DOCWHISPR_WEB_BASE ||
21
+ process.env.NEXT_PUBLIC_APP_URL ||
22
+ "https://docwhispr.com").replace(/\/$/, "");
23
+ function requireApiKey() {
24
+ const key = (process.env.HILIFLOW_API_KEY || "").trim();
25
+ if (!key) {
26
+ throw new Error("HILIFLOW_API_KEY is required. Create one via POST /api/mcp/keys (signed-in browser session), then set it in MCP env.");
27
+ }
28
+ return key;
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;
59
+ }
60
+ function textResult(payload) {
61
+ return {
62
+ content: [
63
+ {
64
+ type: "text",
65
+ text: typeof payload === "string" ? payload : JSON.stringify(payload, null, 2)
66
+ }
67
+ ]
68
+ };
69
+ }
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
+ const server = new McpServer({
91
+ name: "hiliflow",
92
+ version: "0.1.0"
93
+ });
94
+ server.tool("hiliflow_credits", {
95
+ plan: z.enum(["free", "pro", "pro_plus"]).optional()
96
+ }, async ({ plan }) => {
97
+ const q = plan ? `?plan=${encodeURIComponent(plan)}` : "";
98
+ const data = await api(`/api/mcp/credits${q}`);
99
+ return textResult(data);
100
+ });
101
+ server.tool("hiliflow_plan_curriculum", {
102
+ pdfPath: z
103
+ .string()
104
+ .describe("Absolute path to a local PDF on the machine running the MCP server"),
105
+ pageMin: z.number().int().min(1),
106
+ pageMax: z.number().int().min(1),
107
+ includeTopics: z
108
+ .array(z.string())
109
+ .optional()
110
+ .describe('e.g. ["EXTEST","INTEST"]'),
111
+ backgroundPolicy: z
112
+ .enum(["none", "necessary_only", "all_in_section"])
113
+ .optional()
114
+ .default("necessary_only"),
115
+ pageNumbering: z.enum(["auto", "pdf", "book"]).optional().default("auto"),
116
+ userPrompt: z.string().optional(),
117
+ maxPagesPerJob: z.number().int().min(1).max(20).optional().default(10),
118
+ title: z.string().optional()
119
+ }, async (args) => {
120
+ const contentHash = await sha256HexOfFile(args.pdfPath);
121
+ const contentBase64 = fileToBase64(args.pdfPath);
122
+ const teachingScope = {
123
+ include_topics: args.includeTopics ?? [],
124
+ background_policy: args.backgroundPolicy ?? "necessary_only",
125
+ page_numbering: args.pageNumbering ?? "auto",
126
+ user_prompt: args.userPrompt ||
127
+ (args.includeTopics?.length
128
+ ? `Focus on: ${args.includeTopics.join(", ")}`
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
+ }
145
+ });
146
+ return textResult({
147
+ note: "Free curriculum preview. Confirm with the user, then call hiliflow_compile_series (or hiliflow_compile_episode for a single job).",
148
+ plan
149
+ });
150
+ });
151
+ server.tool("hiliflow_compile_episode", {
152
+ pdfPath: z.string(),
153
+ pageMin: z.number().int().min(1),
154
+ pageMax: z.number().int().min(1),
155
+ includeTopics: z.array(z.string()).optional(),
156
+ backgroundPolicy: z
157
+ .enum(["none", "necessary_only", "all_in_section"])
158
+ .optional()
159
+ .default("necessary_only"),
160
+ pageNumbering: z.enum(["auto", "pdf", "book"]).optional().default("auto"),
161
+ userPrompt: z.string().optional(),
162
+ title: z.string().optional(),
163
+ contentLanguage: z.string().optional().default("en"),
164
+ plan: z.enum(["free", "pro", "pro_plus"]).optional().default("free")
165
+ }, async (args) => {
166
+ const contentHash = await sha256HexOfFile(args.pdfPath);
167
+ const contentBase64 = fileToBase64(args.pdfPath);
168
+ const teachingScope = {
169
+ include_topics: args.includeTopics ?? [],
170
+ background_policy: args.backgroundPolicy ?? "necessary_only",
171
+ page_numbering: args.pageNumbering ?? "auto",
172
+ user_prompt: args.userPrompt || ""
173
+ };
174
+ const job = await api("/api/jobs", {
175
+ method: "POST",
176
+ json: {
177
+ fileName: resolve(args.pdfPath).split(/[/\\]/).pop(),
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
+ }
190
+ });
191
+ const jobId = String(job.id || "");
192
+ return textResult({
193
+ job,
194
+ studioUrl: jobId ? `${WEB_BASE}/app/projects/${jobId}` : null,
195
+ note: "1 episode credit charged. Poll hiliflow_job_status until status=succeeded."
196
+ });
197
+ });
198
+ server.tool("hiliflow_compile_series", {
199
+ pdfPath: z.string(),
200
+ pageMin: z.number().int().min(1),
201
+ pageMax: z.number().int().min(1),
202
+ includeTopics: z.array(z.string()).optional(),
203
+ backgroundPolicy: z
204
+ .enum(["none", "necessary_only", "all_in_section"])
205
+ .optional()
206
+ .default("necessary_only"),
207
+ pageNumbering: z.enum(["auto", "pdf", "book"]).optional().default("auto"),
208
+ userPrompt: z.string().optional(),
209
+ maxPagesPerJob: z.number().int().min(1).max(20).optional().default(10),
210
+ title: z.string().optional(),
211
+ contentLanguage: z.string().optional().default("en"),
212
+ plan: z.enum(["free", "pro", "pro_plus"]).optional().default("free")
213
+ }, async (args) => {
214
+ const contentHash = await sha256HexOfFile(args.pdfPath);
215
+ 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
+ };
222
+ const result = await api("/api/series/create", {
223
+ method: "POST",
224
+ json: {
225
+ fileName: resolve(args.pdfPath).split(/[/\\]/).pop(),
226
+ pageMin: args.pageMin,
227
+ pageMax: args.pageMax,
228
+ contentHash,
229
+ contentBase64,
230
+ sourceType: "pdf",
231
+ title: args.title,
232
+ teachingScope,
233
+ maxPagesPerJob: args.maxPagesPerJob ?? 10,
234
+ contentLanguage: args.contentLanguage ?? "en",
235
+ plan: args.plan ?? "free",
236
+ extractDocName: true,
237
+ highlight: true
238
+ }
239
+ });
240
+ return textResult({
241
+ ...result,
242
+ studioUrls: (result.jobs || []).map((j) => ({
243
+ jobId: j.id,
244
+ title: j.title,
245
+ studioUrl: `${WEB_BASE}/app/projects/${j.id}`
246
+ })),
247
+ note: `${result.totalEpisodes} episode credit(s) charged.`
248
+ });
249
+ });
250
+ server.tool("hiliflow_job_status", {
251
+ 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
+ });
259
+ server.tool("hiliflow_open_studio", {
260
+ jobId: z.string()
261
+ }, async ({ jobId }) => {
262
+ const data = await api(`/api/mcp/studio/${jobId}`);
263
+ return textResult(data);
264
+ });
265
+ // silence unused schema warning in some bundlers
266
+ void teachingScopeSchema;
267
+ const transport = new StdioServerTransport();
268
+ await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,55 @@
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.0",
4
+ "description": "docwhispr MCP server — plan curriculum, compile whiteboard lesson videos, open interactive studio",
5
+ "type": "module",
6
+ "bin": {
7
+ "docwhispr-mcp": "dist/index.js",
8
+ "hiliflow-mcp": "dist/index.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "prepublishOnly": "npm run build",
18
+ "start": "node dist/index.js",
19
+ "dev": "tsx src/index.ts",
20
+ "typecheck": "tsc -p tsconfig.json --noEmit"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "docwhispr",
28
+ "hiliflow",
29
+ "curriculum",
30
+ "whiteboard",
31
+ "pdf"
32
+ ],
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/zhenjun-avatar/docwhispr.git",
37
+ "directory": "packages/mcp-server"
38
+ },
39
+ "homepage": "https://docwhispr.com",
40
+ "bugs": {
41
+ "url": "https://github.com/zhenjun-avatar/docwhispr/issues"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.11.2",
48
+ "zod": "^3.24.2"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.13.10",
52
+ "tsx": "^4.19.3",
53
+ "typescript": "^5.8.2"
54
+ }
55
+ }