huozi-mcp-server 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/dist/api.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ interface ApiResponse {
2
+ ok: boolean;
3
+ status: number;
4
+ data: Record<string, unknown>;
5
+ }
6
+ export declare function apiCall(path: string, options?: {
7
+ method?: string;
8
+ body?: Record<string, unknown>;
9
+ token?: string;
10
+ apiKey?: string;
11
+ }): Promise<ApiResponse>;
12
+ export declare function getApiKey(): string | undefined;
13
+ export {};
package/dist/api.js ADDED
@@ -0,0 +1,22 @@
1
+ const BASE_URL = process.env.HUOZI_BASE_URL || "https://huozi.app";
2
+ export async function apiCall(path, options = {}) {
3
+ const headers = {
4
+ "Content-Type": "application/json",
5
+ };
6
+ if (options.apiKey) {
7
+ headers["Authorization"] = `Bearer ${options.apiKey}`;
8
+ }
9
+ else if (options.token) {
10
+ headers["Authorization"] = `Bearer ${options.token}`;
11
+ }
12
+ const res = await fetch(`${BASE_URL}${path}`, {
13
+ method: options.method || "GET",
14
+ headers,
15
+ body: options.body ? JSON.stringify(options.body) : undefined,
16
+ });
17
+ const data = await res.json();
18
+ return { ok: res.ok, status: res.status, data };
19
+ }
20
+ export function getApiKey() {
21
+ return process.env.HUOZI_API_KEY;
22
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { apiCall, getApiKey } from "./api.js";
6
+ const server = new McpServer({
7
+ name: "huozi",
8
+ version: "0.1.0",
9
+ });
10
+ // --- Auth tools ---
11
+ server.tool("huozi_signup", "Register a new Huozi account. Sends a verification code to the email.", {
12
+ email: z.string().email().describe("User email address"),
13
+ password: z
14
+ .string()
15
+ .min(6)
16
+ .describe("Password (minimum 6 characters)"),
17
+ }, async ({ email, password }) => {
18
+ const res = await apiCall("/api/v1/auth/signup", {
19
+ method: "POST",
20
+ body: { email, password },
21
+ });
22
+ if (!res.ok) {
23
+ return {
24
+ content: [
25
+ {
26
+ type: "text",
27
+ text: `Signup failed: ${res.data.error || "Unknown error"}`,
28
+ },
29
+ ],
30
+ };
31
+ }
32
+ return {
33
+ content: [
34
+ {
35
+ type: "text",
36
+ text: `Verification code sent to ${email}. Ask the user to check their email and provide the code, then call huozi_verify.`,
37
+ },
38
+ ],
39
+ };
40
+ });
41
+ server.tool("huozi_verify", "Verify email with the code received. Returns an access_token for setup.", {
42
+ email: z.string().email().describe("Email used during signup"),
43
+ code: z.string().min(6).max(8).describe("Verification code from email"),
44
+ }, async ({ email, code }) => {
45
+ const res = await apiCall("/api/v1/auth/verify", {
46
+ method: "POST",
47
+ body: { email, code },
48
+ });
49
+ if (!res.ok) {
50
+ return {
51
+ content: [
52
+ {
53
+ type: "text",
54
+ text: `Verification failed: ${res.data.error || "Invalid code"}`,
55
+ },
56
+ ],
57
+ };
58
+ }
59
+ return {
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: `Email verified! Access token: ${res.data.access_token}\n\nNow call huozi_setup with this token and a workspace slug to complete setup.`,
64
+ },
65
+ ],
66
+ };
67
+ });
68
+ server.tool("huozi_setup", "Create a workspace and generate an API key. Requires the access_token from huozi_verify.", {
69
+ access_token: z.string().describe("Access token from huozi_verify"),
70
+ workspace_slug: z
71
+ .string()
72
+ .regex(/^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$/)
73
+ .describe("Workspace slug (lowercase, hyphens, 1-40 chars). This becomes huozi.app/<slug>"),
74
+ }, async ({ access_token, workspace_slug }) => {
75
+ const res = await apiCall("/api/v1/auth/setup", {
76
+ method: "POST",
77
+ body: { workspace_slug },
78
+ token: access_token,
79
+ });
80
+ if (!res.ok) {
81
+ return {
82
+ content: [
83
+ {
84
+ type: "text",
85
+ text: `Setup failed: ${res.data.error || "Unknown error"}`,
86
+ },
87
+ ],
88
+ };
89
+ }
90
+ const workspace = res.data.workspace;
91
+ return {
92
+ content: [
93
+ {
94
+ type: "text",
95
+ text: `Setup complete!\n\nWorkspace: ${workspace.url}\nAPI Key: ${res.data.api_key}\n\nTell the user to set HUOZI_API_KEY=${res.data.api_key} in their environment. They can now publish pages with huozi_publish.`,
96
+ },
97
+ ],
98
+ };
99
+ });
100
+ // --- Page tools ---
101
+ server.tool("huozi_publish", "Publish or update a Markdown page on Huozi. If a page with the same slug exists, it will be updated.", {
102
+ title: z.string().min(1).describe("Page title"),
103
+ content: z.string().min(1).describe("Markdown content"),
104
+ slug: z
105
+ .string()
106
+ .optional()
107
+ .describe("URL slug (optional, auto-generated from title if omitted)"),
108
+ description: z
109
+ .string()
110
+ .optional()
111
+ .describe("SEO description (optional)"),
112
+ }, async ({ title, content, slug, description }) => {
113
+ const apiKey = getApiKey();
114
+ if (!apiKey) {
115
+ return {
116
+ content: [
117
+ {
118
+ type: "text",
119
+ text: "HUOZI_API_KEY is not set. Please run huozi_signup → huozi_verify → huozi_setup first, or set the environment variable.",
120
+ },
121
+ ],
122
+ };
123
+ }
124
+ const body = { title, content };
125
+ if (slug)
126
+ body.slug = slug;
127
+ if (description)
128
+ body.description = description;
129
+ const res = await apiCall("/api/v1/pages", {
130
+ method: "POST",
131
+ body,
132
+ apiKey,
133
+ });
134
+ if (!res.ok) {
135
+ return {
136
+ content: [
137
+ {
138
+ type: "text",
139
+ text: `Publish failed: ${res.data.error || "Unknown error"}`,
140
+ },
141
+ ],
142
+ };
143
+ }
144
+ return {
145
+ content: [
146
+ {
147
+ type: "text",
148
+ text: `Published! URL: ${res.data.url}`,
149
+ },
150
+ ],
151
+ };
152
+ });
153
+ server.tool("huozi_list", "List all pages in the workspace.", {}, async () => {
154
+ const apiKey = getApiKey();
155
+ if (!apiKey) {
156
+ return {
157
+ content: [
158
+ {
159
+ type: "text",
160
+ text: "HUOZI_API_KEY is not set.",
161
+ },
162
+ ],
163
+ };
164
+ }
165
+ const res = await apiCall("/api/v1/pages", { apiKey });
166
+ if (!res.ok) {
167
+ return {
168
+ content: [
169
+ {
170
+ type: "text",
171
+ text: `Failed to list pages: ${res.data.error || "Unknown error"}`,
172
+ },
173
+ ],
174
+ };
175
+ }
176
+ const pages = res.data.pages;
177
+ if (!pages || pages.length === 0) {
178
+ return {
179
+ content: [{ type: "text", text: "No pages yet." }],
180
+ };
181
+ }
182
+ const list = pages
183
+ .map((p) => `- ${p.title} (/${p.slug}) ${p.is_published ? "✓" : "[draft]"} — ${p.updated_at}`)
184
+ .join("\n");
185
+ return {
186
+ content: [{ type: "text", text: list }],
187
+ };
188
+ });
189
+ server.tool("huozi_get", "Get details and content of a specific page.", {
190
+ slug: z.string().describe("Page slug"),
191
+ }, async ({ slug }) => {
192
+ const apiKey = getApiKey();
193
+ if (!apiKey) {
194
+ return {
195
+ content: [
196
+ { type: "text", text: "HUOZI_API_KEY is not set." },
197
+ ],
198
+ };
199
+ }
200
+ const res = await apiCall(`/api/v1/pages/${slug}`, { apiKey });
201
+ if (!res.ok) {
202
+ return {
203
+ content: [
204
+ {
205
+ type: "text",
206
+ text: `Page not found: ${res.data.error || slug}`,
207
+ },
208
+ ],
209
+ };
210
+ }
211
+ return {
212
+ content: [
213
+ {
214
+ type: "text",
215
+ text: `Title: ${res.data.title}\nSlug: ${res.data.slug}\nPublished: ${res.data.is_published}\nUpdated: ${res.data.updated_at}\n\n---\n\n${res.data.content}`,
216
+ },
217
+ ],
218
+ };
219
+ });
220
+ server.tool("huozi_delete", "Delete a page.", {
221
+ slug: z.string().describe("Page slug to delete"),
222
+ }, async ({ slug }) => {
223
+ const apiKey = getApiKey();
224
+ if (!apiKey) {
225
+ return {
226
+ content: [
227
+ { type: "text", text: "HUOZI_API_KEY is not set." },
228
+ ],
229
+ };
230
+ }
231
+ const res = await apiCall(`/api/v1/pages/${slug}`, {
232
+ method: "DELETE",
233
+ apiKey,
234
+ });
235
+ if (!res.ok) {
236
+ return {
237
+ content: [
238
+ {
239
+ type: "text",
240
+ text: `Delete failed: ${res.data.error || "Unknown error"}`,
241
+ },
242
+ ],
243
+ };
244
+ }
245
+ return {
246
+ content: [
247
+ { type: "text", text: `Deleted page: ${slug}` },
248
+ ],
249
+ };
250
+ });
251
+ // --- Start server ---
252
+ async function main() {
253
+ const transport = new StdioServerTransport();
254
+ await server.connect(transport);
255
+ }
256
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "huozi-mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Huozi — publish Markdown as shareable web pages",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "huozi-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsc --watch",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "markdown",
21
+ "publishing",
22
+ "huozi",
23
+ "ai-agent",
24
+ "claude-code"
25
+ ],
26
+ "author": "Dachein",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.12.1"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20",
33
+ "typescript": "^5"
34
+ }
35
+ }