@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 +75 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +268 -0
- package/package.json +55 -6
package/README.md
CHANGED
|
@@ -1,3 +1,75 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
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 |
|
package/dist/index.d.ts
ADDED
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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|