@biaoo/tiangong-wiki 0.2.2 → 0.3.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 +106 -1
- package/README.zh-CN.md +106 -1
- package/dist/commands/create.js +3 -0
- package/dist/commands/sync.js +3 -0
- package/dist/commands/template.js +3 -0
- package/dist/core/codex-workflow.js +37 -15
- package/dist/core/db.js +19 -0
- package/dist/core/onboarding.js +35 -1
- package/dist/core/page-source.js +25 -0
- package/dist/core/paths.js +10 -0
- package/dist/core/workflow-result.js +5 -0
- package/dist/daemon/audit-log.js +18 -0
- package/dist/daemon/client.js +1 -1
- package/dist/daemon/git-journal.js +114 -0
- package/dist/daemon/server.js +446 -124
- package/dist/daemon/write-actor.js +60 -0
- package/dist/daemon/write-queue.js +360 -0
- package/dist/operations/dashboard.js +4 -9
- package/dist/operations/write.js +93 -5
- package/mcp-server/dist/daemon-client.js +90 -0
- package/mcp-server/dist/index.js +26 -0
- package/mcp-server/dist/server.js +525 -0
- package/package.json +11 -5
- package/references/centralized-service-deployment.md +482 -0
- package/references/examples/centralized-service/centralized.env.example +25 -0
- package/references/examples/centralized-service/nginx-centralized-wiki.conf +68 -0
- package/references/examples/centralized-service/tiangong-wiki-daemon.service +17 -0
- package/references/examples/centralized-service/tiangong-wiki-mcp.service +18 -0
- package/references/troubleshooting.md +22 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
6
|
+
import { DaemonHttpError, requestDaemonJson } from "./daemon-client.js";
|
|
7
|
+
const limitSchema = z.coerce.number().int().positive().max(1000).optional();
|
|
8
|
+
const pageIdSchema = z.string().trim().min(1);
|
|
9
|
+
const stringMapSchema = z.record(z.string(), z.union([z.string(), z.number(), z.boolean()]));
|
|
10
|
+
const frontmatterPatchSchema = z.record(z.string(), z.unknown());
|
|
11
|
+
function parsePort(rawValue) {
|
|
12
|
+
if (!rawValue || !rawValue.trim()) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const value = Number.parseInt(rawValue.trim(), 10);
|
|
16
|
+
if (!Number.isInteger(value) || value < 0 || value > 65535) {
|
|
17
|
+
throw new Error(`Invalid port value: ${rawValue}`);
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function normalizeMcpPath(rawValue) {
|
|
22
|
+
const value = rawValue?.trim() || "/mcp";
|
|
23
|
+
const normalized = `/${value.replace(/^\/+/, "")}`;
|
|
24
|
+
return normalized === "/" ? "/mcp" : normalized;
|
|
25
|
+
}
|
|
26
|
+
function headerValue(headers, name) {
|
|
27
|
+
const value = headers?.[name.toLowerCase()];
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
const normalized = value.trim();
|
|
30
|
+
return normalized ? normalized : null;
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
const normalized = value.find((entry) => typeof entry === "string" && entry.trim());
|
|
34
|
+
return normalized ? normalized.trim() : null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function buildWriteActorHeaders(headers) {
|
|
39
|
+
const actorId = headerValue(headers, "x-wiki-actor-id");
|
|
40
|
+
const actorType = headerValue(headers, "x-wiki-actor-type");
|
|
41
|
+
const requestId = headerValue(headers, "x-request-id");
|
|
42
|
+
if (!actorId || !actorType || !requestId) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
"x-wiki-actor-id": actorId,
|
|
47
|
+
"x-wiki-actor-type": actorType,
|
|
48
|
+
"x-request-id": requestId,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function jsonText(value) {
|
|
52
|
+
return JSON.stringify(value, null, 2);
|
|
53
|
+
}
|
|
54
|
+
function toStructuredContent(payload) {
|
|
55
|
+
if (typeof payload === "object" && payload !== null && !Array.isArray(payload)) {
|
|
56
|
+
return payload;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
result: payload,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function toolSuccess(payload) {
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text: jsonText(payload) }],
|
|
65
|
+
structuredContent: toStructuredContent(payload),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function toolError(payload) {
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: "text", text: jsonText(payload) }],
|
|
71
|
+
structuredContent: payload,
|
|
72
|
+
isError: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function missingActorError() {
|
|
76
|
+
return toolError({
|
|
77
|
+
code: "missing_actor",
|
|
78
|
+
type: "config",
|
|
79
|
+
message: "Write tools require proxy-injected headers x-wiki-actor-id, x-wiki-actor-type, and x-request-id.",
|
|
80
|
+
details: {
|
|
81
|
+
requiredHeaders: ["x-wiki-actor-id", "x-wiki-actor-type", "x-request-id"],
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function normalizeErrorPayload(error) {
|
|
86
|
+
if (error instanceof DaemonHttpError) {
|
|
87
|
+
const details = typeof error.details === "object" && error.details !== null && !Array.isArray(error.details)
|
|
88
|
+
? { ...error.details }
|
|
89
|
+
: {};
|
|
90
|
+
const code = typeof details.code === "string"
|
|
91
|
+
? details.code
|
|
92
|
+
: error.type === "not_configured"
|
|
93
|
+
? "not_configured"
|
|
94
|
+
: error.type === "config"
|
|
95
|
+
? "invalid_request"
|
|
96
|
+
: error.httpStatus === 404
|
|
97
|
+
? "not_found"
|
|
98
|
+
: error.httpStatus === 409
|
|
99
|
+
? "conflict"
|
|
100
|
+
: "runtime_error";
|
|
101
|
+
return {
|
|
102
|
+
code,
|
|
103
|
+
message: error.message,
|
|
104
|
+
type: error.type,
|
|
105
|
+
httpStatus: error.httpStatus,
|
|
106
|
+
pageId: typeof details.pageId === "string" ? details.pageId : undefined,
|
|
107
|
+
currentRevision: typeof details.currentRevision === "string" || details.currentRevision === null
|
|
108
|
+
? details.currentRevision
|
|
109
|
+
: undefined,
|
|
110
|
+
degraded: details.degraded === true,
|
|
111
|
+
details,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
code: "runtime_error",
|
|
116
|
+
type: "runtime",
|
|
117
|
+
message: error instanceof Error ? error.message : String(error),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
async function runReadTool(request) {
|
|
121
|
+
try {
|
|
122
|
+
const payload = await requestDaemonJson({
|
|
123
|
+
env: request.env,
|
|
124
|
+
method: request.method ?? "GET",
|
|
125
|
+
path: request.path,
|
|
126
|
+
query: request.query,
|
|
127
|
+
body: request.body,
|
|
128
|
+
});
|
|
129
|
+
return toolSuccess(payload);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return toolError(normalizeErrorPayload(error));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function runWriteTool(env, headers, request) {
|
|
136
|
+
const actorHeaders = buildWriteActorHeaders(headers);
|
|
137
|
+
if (!actorHeaders) {
|
|
138
|
+
return missingActorError();
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const payload = await requestDaemonJson({
|
|
142
|
+
env,
|
|
143
|
+
method: "POST",
|
|
144
|
+
path: request.path,
|
|
145
|
+
headers: actorHeaders,
|
|
146
|
+
body: request.body,
|
|
147
|
+
});
|
|
148
|
+
return toolSuccess(payload);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return toolError(normalizeErrorPayload(error));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function createWikiMcpServer(env) {
|
|
155
|
+
const server = new McpServer({
|
|
156
|
+
name: packageJson.name,
|
|
157
|
+
version: packageJson.version,
|
|
158
|
+
}, {
|
|
159
|
+
instructions: "This is a thin MCP adapter for the Tiangong Wiki daemon. All reads and writes must go through the daemon. Write tools require proxy-injected actor headers.",
|
|
160
|
+
});
|
|
161
|
+
server.registerTool("wiki_find", {
|
|
162
|
+
title: "Wiki Find",
|
|
163
|
+
description: "Find wiki pages by structured metadata filters via the daemon /find endpoint.",
|
|
164
|
+
inputSchema: z.object({
|
|
165
|
+
type: z.string().trim().min(1).optional(),
|
|
166
|
+
status: z.string().trim().min(1).optional(),
|
|
167
|
+
visibility: z.string().trim().min(1).optional(),
|
|
168
|
+
tag: z.string().trim().min(1).optional(),
|
|
169
|
+
nodeId: z.string().trim().min(1).optional(),
|
|
170
|
+
updatedAfter: z.string().trim().min(1).optional(),
|
|
171
|
+
sort: z.string().trim().min(1).optional(),
|
|
172
|
+
limit: limitSchema,
|
|
173
|
+
extraFilters: stringMapSchema.optional(),
|
|
174
|
+
}),
|
|
175
|
+
annotations: {
|
|
176
|
+
readOnlyHint: true,
|
|
177
|
+
idempotentHint: true,
|
|
178
|
+
},
|
|
179
|
+
}, async (args) => runReadTool({
|
|
180
|
+
env,
|
|
181
|
+
path: "/find",
|
|
182
|
+
query: {
|
|
183
|
+
type: args.type,
|
|
184
|
+
status: args.status,
|
|
185
|
+
visibility: args.visibility,
|
|
186
|
+
tag: args.tag,
|
|
187
|
+
nodeId: args.nodeId,
|
|
188
|
+
updatedAfter: args.updatedAfter,
|
|
189
|
+
sort: args.sort,
|
|
190
|
+
limit: args.limit,
|
|
191
|
+
...(args.extraFilters ?? {}),
|
|
192
|
+
},
|
|
193
|
+
}));
|
|
194
|
+
server.registerTool("wiki_fts", {
|
|
195
|
+
title: "Wiki FTS",
|
|
196
|
+
description: "Run full-text search via the daemon /fts endpoint.",
|
|
197
|
+
inputSchema: z.object({
|
|
198
|
+
query: z.string().trim().min(1),
|
|
199
|
+
type: z.string().trim().min(1).optional(),
|
|
200
|
+
limit: limitSchema,
|
|
201
|
+
}),
|
|
202
|
+
annotations: {
|
|
203
|
+
readOnlyHint: true,
|
|
204
|
+
idempotentHint: true,
|
|
205
|
+
},
|
|
206
|
+
}, async (args) => runReadTool({
|
|
207
|
+
env,
|
|
208
|
+
path: "/fts",
|
|
209
|
+
query: {
|
|
210
|
+
query: args.query,
|
|
211
|
+
type: args.type,
|
|
212
|
+
limit: args.limit,
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
server.registerTool("wiki_search", {
|
|
216
|
+
title: "Wiki Search",
|
|
217
|
+
description: "Run semantic search via the daemon /search endpoint.",
|
|
218
|
+
inputSchema: z.object({
|
|
219
|
+
query: z.string().trim().min(1),
|
|
220
|
+
type: z.string().trim().min(1).optional(),
|
|
221
|
+
limit: limitSchema,
|
|
222
|
+
}),
|
|
223
|
+
annotations: {
|
|
224
|
+
readOnlyHint: true,
|
|
225
|
+
idempotentHint: true,
|
|
226
|
+
},
|
|
227
|
+
}, async (args) => runReadTool({
|
|
228
|
+
env,
|
|
229
|
+
path: "/search",
|
|
230
|
+
query: {
|
|
231
|
+
query: args.query,
|
|
232
|
+
type: args.type,
|
|
233
|
+
limit: args.limit,
|
|
234
|
+
},
|
|
235
|
+
}));
|
|
236
|
+
server.registerTool("wiki_graph", {
|
|
237
|
+
title: "Wiki Graph",
|
|
238
|
+
description: "Traverse the wiki graph via the daemon /graph endpoint.",
|
|
239
|
+
inputSchema: z.object({
|
|
240
|
+
root: z.string().trim().min(1),
|
|
241
|
+
depth: z.coerce.number().int().positive().max(16).optional(),
|
|
242
|
+
edgeType: z.string().trim().min(1).optional(),
|
|
243
|
+
direction: z.enum(["outgoing", "incoming", "both"]).optional(),
|
|
244
|
+
}),
|
|
245
|
+
annotations: {
|
|
246
|
+
readOnlyHint: true,
|
|
247
|
+
idempotentHint: true,
|
|
248
|
+
},
|
|
249
|
+
}, async (args) => runReadTool({
|
|
250
|
+
env,
|
|
251
|
+
path: "/graph",
|
|
252
|
+
query: {
|
|
253
|
+
root: args.root,
|
|
254
|
+
depth: args.depth,
|
|
255
|
+
edgeType: args.edgeType,
|
|
256
|
+
direction: args.direction,
|
|
257
|
+
},
|
|
258
|
+
}));
|
|
259
|
+
server.registerTool("wiki_type_list", {
|
|
260
|
+
title: "Wiki Type List",
|
|
261
|
+
description: "List registered wiki page types via the daemon /type/list endpoint.",
|
|
262
|
+
inputSchema: z.object({}),
|
|
263
|
+
annotations: {
|
|
264
|
+
readOnlyHint: true,
|
|
265
|
+
idempotentHint: true,
|
|
266
|
+
},
|
|
267
|
+
}, async () => runReadTool({
|
|
268
|
+
env,
|
|
269
|
+
path: "/type/list",
|
|
270
|
+
}));
|
|
271
|
+
server.registerTool("wiki_type_show", {
|
|
272
|
+
title: "Wiki Type Show",
|
|
273
|
+
description: "Read one wiki page type definition via the daemon /type/show endpoint.",
|
|
274
|
+
inputSchema: z.object({
|
|
275
|
+
pageType: z.string().trim().min(1),
|
|
276
|
+
}),
|
|
277
|
+
annotations: {
|
|
278
|
+
readOnlyHint: true,
|
|
279
|
+
idempotentHint: true,
|
|
280
|
+
},
|
|
281
|
+
}, async (args) => runReadTool({
|
|
282
|
+
env,
|
|
283
|
+
path: "/type/show",
|
|
284
|
+
query: {
|
|
285
|
+
pageType: args.pageType,
|
|
286
|
+
},
|
|
287
|
+
}));
|
|
288
|
+
server.registerTool("wiki_type_recommend", {
|
|
289
|
+
title: "Wiki Type Recommend",
|
|
290
|
+
description: "Recommend wiki page types via the daemon /type/recommend endpoint.",
|
|
291
|
+
inputSchema: z.object({
|
|
292
|
+
text: z.string().trim().min(1),
|
|
293
|
+
keywords: z.string().trim().min(1).optional(),
|
|
294
|
+
limit: limitSchema,
|
|
295
|
+
}),
|
|
296
|
+
annotations: {
|
|
297
|
+
readOnlyHint: true,
|
|
298
|
+
idempotentHint: true,
|
|
299
|
+
},
|
|
300
|
+
}, async (args) => runReadTool({
|
|
301
|
+
env,
|
|
302
|
+
method: "POST",
|
|
303
|
+
path: "/type/recommend",
|
|
304
|
+
body: {
|
|
305
|
+
text: args.text,
|
|
306
|
+
keywords: args.keywords,
|
|
307
|
+
limit: args.limit,
|
|
308
|
+
},
|
|
309
|
+
}));
|
|
310
|
+
server.registerTool("wiki_vault_list", {
|
|
311
|
+
title: "Wiki Vault List",
|
|
312
|
+
description: "List indexed vault files via the daemon /vault/list endpoint.",
|
|
313
|
+
inputSchema: z.object({
|
|
314
|
+
path: z.string().trim().min(1).optional(),
|
|
315
|
+
ext: z.string().trim().min(1).optional(),
|
|
316
|
+
}),
|
|
317
|
+
annotations: {
|
|
318
|
+
readOnlyHint: true,
|
|
319
|
+
idempotentHint: true,
|
|
320
|
+
},
|
|
321
|
+
}, async (args) => runReadTool({
|
|
322
|
+
env,
|
|
323
|
+
path: "/vault/list",
|
|
324
|
+
query: {
|
|
325
|
+
path: args.path,
|
|
326
|
+
ext: args.ext,
|
|
327
|
+
},
|
|
328
|
+
}));
|
|
329
|
+
server.registerTool("wiki_vault_queue", {
|
|
330
|
+
title: "Wiki Vault Queue",
|
|
331
|
+
description: "Read vault processing queue status via the daemon /vault/queue endpoint.",
|
|
332
|
+
inputSchema: z.object({
|
|
333
|
+
status: z.string().trim().min(1).optional(),
|
|
334
|
+
}),
|
|
335
|
+
annotations: {
|
|
336
|
+
readOnlyHint: true,
|
|
337
|
+
idempotentHint: true,
|
|
338
|
+
},
|
|
339
|
+
}, async (args) => runReadTool({
|
|
340
|
+
env,
|
|
341
|
+
path: "/vault/queue",
|
|
342
|
+
query: {
|
|
343
|
+
status: args.status,
|
|
344
|
+
},
|
|
345
|
+
}));
|
|
346
|
+
server.registerTool("wiki_page_info", {
|
|
347
|
+
title: "Wiki Page Info",
|
|
348
|
+
description: "Read page metadata and edges via the daemon /page-info endpoint.",
|
|
349
|
+
inputSchema: z.object({
|
|
350
|
+
pageId: pageIdSchema,
|
|
351
|
+
}),
|
|
352
|
+
annotations: {
|
|
353
|
+
readOnlyHint: true,
|
|
354
|
+
idempotentHint: true,
|
|
355
|
+
},
|
|
356
|
+
}, async (args) => runReadTool({
|
|
357
|
+
env,
|
|
358
|
+
path: "/page-info",
|
|
359
|
+
query: {
|
|
360
|
+
pageId: args.pageId,
|
|
361
|
+
},
|
|
362
|
+
}));
|
|
363
|
+
server.registerTool("wiki_page_read", {
|
|
364
|
+
title: "Wiki Page Read",
|
|
365
|
+
description: "Read canonical page source and revision via the daemon /page-read endpoint.",
|
|
366
|
+
inputSchema: z.object({
|
|
367
|
+
pageId: pageIdSchema,
|
|
368
|
+
}),
|
|
369
|
+
annotations: {
|
|
370
|
+
readOnlyHint: true,
|
|
371
|
+
idempotentHint: true,
|
|
372
|
+
},
|
|
373
|
+
}, async (args) => runReadTool({
|
|
374
|
+
env,
|
|
375
|
+
path: "/page-read",
|
|
376
|
+
query: {
|
|
377
|
+
pageId: args.pageId,
|
|
378
|
+
},
|
|
379
|
+
}));
|
|
380
|
+
server.registerTool("wiki_sync", {
|
|
381
|
+
title: "Wiki Sync",
|
|
382
|
+
description: "Run daemon sync via the /sync endpoint with the daemon write queue and audit flow.",
|
|
383
|
+
inputSchema: z.object({
|
|
384
|
+
path: z.string().trim().min(1).optional(),
|
|
385
|
+
force: z.boolean().optional(),
|
|
386
|
+
skipEmbedding: z.boolean().optional(),
|
|
387
|
+
process: z.boolean().optional(),
|
|
388
|
+
vaultFileId: z.string().trim().min(1).optional(),
|
|
389
|
+
}),
|
|
390
|
+
annotations: {
|
|
391
|
+
destructiveHint: true,
|
|
392
|
+
},
|
|
393
|
+
}, async (args, extra) => runWriteTool(env, extra.requestInfo?.headers, {
|
|
394
|
+
path: "/sync",
|
|
395
|
+
body: {
|
|
396
|
+
path: args.path,
|
|
397
|
+
force: args.force === true,
|
|
398
|
+
skipEmbedding: args.skipEmbedding === true,
|
|
399
|
+
process: args.process === true,
|
|
400
|
+
vaultFileId: args.vaultFileId,
|
|
401
|
+
},
|
|
402
|
+
}));
|
|
403
|
+
server.registerTool("wiki_page_create", {
|
|
404
|
+
title: "Wiki Page Create",
|
|
405
|
+
description: "Create a wiki page via the daemon /create endpoint.",
|
|
406
|
+
inputSchema: z.object({
|
|
407
|
+
type: z.string().trim().min(1),
|
|
408
|
+
title: z.string().trim().min(1),
|
|
409
|
+
nodeId: z.string().trim().min(1).optional(),
|
|
410
|
+
}),
|
|
411
|
+
annotations: {
|
|
412
|
+
destructiveHint: true,
|
|
413
|
+
},
|
|
414
|
+
}, async (args, extra) => runWriteTool(env, extra.requestInfo?.headers, {
|
|
415
|
+
path: "/create",
|
|
416
|
+
body: {
|
|
417
|
+
type: args.type,
|
|
418
|
+
title: args.title,
|
|
419
|
+
nodeId: args.nodeId,
|
|
420
|
+
},
|
|
421
|
+
}));
|
|
422
|
+
server.registerTool("wiki_page_update", {
|
|
423
|
+
title: "Wiki Page Update",
|
|
424
|
+
description: "Update a wiki page via the daemon /page-update endpoint.",
|
|
425
|
+
inputSchema: z.object({
|
|
426
|
+
pageId: pageIdSchema,
|
|
427
|
+
bodyMarkdown: z.string().optional(),
|
|
428
|
+
frontmatterPatch: frontmatterPatchSchema.optional(),
|
|
429
|
+
ifRevision: z.string().trim().min(1).optional(),
|
|
430
|
+
}),
|
|
431
|
+
annotations: {
|
|
432
|
+
destructiveHint: true,
|
|
433
|
+
},
|
|
434
|
+
}, async (args, extra) => runWriteTool(env, extra.requestInfo?.headers, {
|
|
435
|
+
path: "/page-update",
|
|
436
|
+
body: {
|
|
437
|
+
pageId: args.pageId,
|
|
438
|
+
bodyMarkdown: args.bodyMarkdown,
|
|
439
|
+
frontmatterPatch: args.frontmatterPatch,
|
|
440
|
+
ifRevision: args.ifRevision,
|
|
441
|
+
},
|
|
442
|
+
}));
|
|
443
|
+
server.registerTool("wiki_lint", {
|
|
444
|
+
title: "Wiki Lint",
|
|
445
|
+
description: "Run wiki lint checks via the daemon /lint endpoint.",
|
|
446
|
+
inputSchema: z.object({
|
|
447
|
+
path: z.string().trim().min(1).optional(),
|
|
448
|
+
level: z.enum(["error", "warning", "info"]).optional(),
|
|
449
|
+
}),
|
|
450
|
+
annotations: {
|
|
451
|
+
readOnlyHint: true,
|
|
452
|
+
idempotentHint: true,
|
|
453
|
+
},
|
|
454
|
+
}, async (args) => runReadTool({
|
|
455
|
+
env,
|
|
456
|
+
path: "/lint",
|
|
457
|
+
query: {
|
|
458
|
+
path: args.path,
|
|
459
|
+
level: args.level,
|
|
460
|
+
},
|
|
461
|
+
}));
|
|
462
|
+
return server;
|
|
463
|
+
}
|
|
464
|
+
function writeJsonResponse(response, statusCode, payload) {
|
|
465
|
+
response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
|
|
466
|
+
response.end(`${JSON.stringify(payload)}\n`);
|
|
467
|
+
}
|
|
468
|
+
async function handleMcpRequest(request, response, mcpPath, env) {
|
|
469
|
+
const requestUrl = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
470
|
+
if (request.method === "GET" && requestUrl.pathname === "/health") {
|
|
471
|
+
writeJsonResponse(response, 200, {
|
|
472
|
+
ok: true,
|
|
473
|
+
service: packageJson.name,
|
|
474
|
+
mcpPath,
|
|
475
|
+
});
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (requestUrl.pathname !== mcpPath) {
|
|
479
|
+
writeJsonResponse(response, 404, {
|
|
480
|
+
error: `Unknown MCP route: ${request.method ?? "GET"} ${requestUrl.pathname}`,
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const server = createWikiMcpServer(env);
|
|
485
|
+
const transport = new StreamableHTTPServerTransport({
|
|
486
|
+
sessionIdGenerator: undefined,
|
|
487
|
+
});
|
|
488
|
+
await server.connect(transport);
|
|
489
|
+
await transport.handleRequest(request, response);
|
|
490
|
+
}
|
|
491
|
+
export async function startMcpHttpServer(env = process.env) {
|
|
492
|
+
const host = env.WIKI_MCP_HOST?.trim() || "127.0.0.1";
|
|
493
|
+
const port = parsePort(env.WIKI_MCP_PORT) ?? 0;
|
|
494
|
+
const mcpPath = normalizeMcpPath(env.WIKI_MCP_PATH);
|
|
495
|
+
const server = createServer((request, response) => {
|
|
496
|
+
void handleMcpRequest(request, response, mcpPath, env).catch((error) => {
|
|
497
|
+
writeJsonResponse(response, 500, {
|
|
498
|
+
error: error instanceof Error ? error.message : String(error),
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
await new Promise((resolve, reject) => {
|
|
503
|
+
server.once("error", reject);
|
|
504
|
+
server.listen(port, host, () => resolve());
|
|
505
|
+
});
|
|
506
|
+
const address = server.address();
|
|
507
|
+
if (!address || typeof address === "string") {
|
|
508
|
+
throw new Error("Failed to determine MCP server listening address.");
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
host,
|
|
512
|
+
port: address.port,
|
|
513
|
+
healthUrl: `http://${host}:${address.port}/health`,
|
|
514
|
+
mcpUrl: `http://${host}:${address.port}${mcpPath}`,
|
|
515
|
+
close: () => new Promise((resolve, reject) => {
|
|
516
|
+
server.close((error) => {
|
|
517
|
+
if (error) {
|
|
518
|
+
reject(error);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
resolve();
|
|
522
|
+
});
|
|
523
|
+
}),
|
|
524
|
+
};
|
|
525
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@biaoo/tiangong-wiki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Local-first wiki index and query engine for Markdown knowledge pages (Tiangong Wiki).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
},
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"bin": {
|
|
19
|
-
"tiangong-wiki": "./dist/index.js"
|
|
19
|
+
"tiangong-wiki": "./dist/index.js",
|
|
20
|
+
"tiangong-wiki-mcp-server": "./mcp-server/dist/index.js"
|
|
20
21
|
},
|
|
21
22
|
"main": "./dist/index.js",
|
|
22
23
|
"files": [
|
|
23
24
|
"dist",
|
|
25
|
+
"mcp-server/dist",
|
|
24
26
|
"assets",
|
|
25
27
|
"references",
|
|
26
28
|
"agents",
|
|
@@ -30,12 +32,14 @@
|
|
|
30
32
|
"node": ">=18"
|
|
31
33
|
},
|
|
32
34
|
"scripts": {
|
|
33
|
-
"build": "npm run build:cli && npm run build:dashboard",
|
|
35
|
+
"build": "npm run build:cli && npm run build:dashboard && npm run build:mcp-server",
|
|
34
36
|
"build:cli": "tsc -p tsconfig.json",
|
|
35
37
|
"build:dashboard": "vite build --config dashboard/vite.config.ts",
|
|
36
|
-
"
|
|
38
|
+
"build:mcp-server": "tsc -p mcp-server/tsconfig.json",
|
|
39
|
+
"clean": "rm -rf dist mcp-server/dist node_modules/.vitest node_modules/.vite",
|
|
37
40
|
"dev": "tsx src/index.ts",
|
|
38
41
|
"dev:dashboard": "vite --config dashboard/vite.config.ts",
|
|
42
|
+
"dev:mcp-server": "tsx mcp-server/src/index.ts",
|
|
39
43
|
"test": "npm run build && vitest run",
|
|
40
44
|
"test:watch": "vitest"
|
|
41
45
|
},
|
|
@@ -44,13 +48,15 @@
|
|
|
44
48
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
45
49
|
"@fontsource/space-grotesk": "^5.2.10",
|
|
46
50
|
"@inquirer/prompts": "^7.10.1",
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
47
52
|
"@openai/codex-sdk": "^0.118.0",
|
|
48
53
|
"adm-zip": "^0.5.17",
|
|
49
54
|
"better-sqlite3": "^12.8.0",
|
|
50
55
|
"commander": "^14.0.3",
|
|
51
56
|
"gray-matter": "^4.0.3",
|
|
52
57
|
"preact": "^10.29.1",
|
|
53
|
-
"sqlite-vec": "^0.1.9"
|
|
58
|
+
"sqlite-vec": "^0.1.9",
|
|
59
|
+
"zod": "^4.3.6"
|
|
54
60
|
},
|
|
55
61
|
"devDependencies": {
|
|
56
62
|
"@preact/preset-vite": "^2.10.5",
|