@hiai-gg/docsmint 0.6.0 → 0.6.2

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.
@@ -14,159 +14,212 @@ import type { ExportResponse } from "./types.js";
14
14
  const DEFAULT_BASE_URL = "http://localhost:50700";
15
15
 
16
16
  function readConfig() {
17
- const baseUrl = (process.env.HIAI_DOCS_URL ?? DEFAULT_BASE_URL).replace(
18
- /\/+$/,
19
- "",
20
- );
21
- const apiKey = process.env.HIAI_DOCS_API_KEY ?? "";
22
- return { baseUrl, apiKey };
17
+ const baseUrl = (process.env.HIAI_DOCS_URL ?? DEFAULT_BASE_URL).replace(
18
+ /\/+$/,
19
+ "",
20
+ );
21
+ const apiKey = process.env.HIAI_DOCS_API_KEY ?? "";
22
+ return { baseUrl, apiKey };
23
23
  }
24
24
 
25
25
  export class HiaiDocsError extends Error {
26
- constructor(
27
- message: string,
28
- readonly status: number,
29
- readonly body: unknown,
30
- ) {
31
- super(message);
32
- this.name = "HiaiDocsError";
33
- }
26
+ constructor(
27
+ message: string,
28
+ readonly status: number,
29
+ readonly body: unknown,
30
+ ) {
31
+ super(message);
32
+ this.name = "HiaiDocsError";
33
+ }
34
34
  }
35
35
 
36
36
  type QueryValue = string | number | boolean | string[] | undefined;
37
37
 
38
38
  async function request<T>(
39
- method: string,
40
- path: string,
41
- options: { query?: Record<string, QueryValue>; body?: unknown } = {},
39
+ method: string,
40
+ path: string,
41
+ options: { query?: Record<string, QueryValue>; body?: unknown } = {},
42
42
  ): Promise<T> {
43
- const { baseUrl, apiKey } = readConfig();
44
-
45
- const url = new URL(path.startsWith("/") ? path : `/${path}`, `${baseUrl}/`);
46
- if (options.query) {
47
- for (const [key, value] of Object.entries(options.query)) {
48
- if (value === undefined) continue;
49
- if (Array.isArray(value)) {
50
- if (value.length > 0) url.searchParams.set(key, value.join(","));
51
- } else {
52
- url.searchParams.set(key, String(value));
53
- }
54
- }
55
- }
56
-
57
- const headers: Record<string, string> = {
58
- Accept: "application/json",
59
- };
60
- if (apiKey) {
61
- headers["Authorization"] = `Bearer ${apiKey}`;
62
- }
63
-
64
- let body: BodyInit | undefined;
65
- if (options.body !== undefined) {
66
- headers["Content-Type"] = "application/json";
67
- body = JSON.stringify(options.body);
68
- }
69
-
70
- const response = await fetch(url, { method, headers, body });
71
-
72
- const contentType = response.headers.get("content-type") ?? "";
73
- const isJson = contentType.includes("application/json");
74
- const payload: unknown = isJson
75
- ? await response.json().catch(() => null)
76
- : await response.text().catch(() => null);
77
-
78
- if (!response.ok) {
79
- const message =
80
- (isJson && payload && typeof payload === "object" && "error" in payload
81
- ? String((payload as { error: unknown }).error)
82
- : typeof payload === "string" && payload.length > 0
83
- ? payload
84
- : `HTTP ${response.status} ${response.statusText}`) ||
85
- `HTTP ${response.status}`;
86
- throw new HiaiDocsError(message, response.status, payload);
87
- }
88
-
89
- return payload as T;
43
+ const { baseUrl, apiKey } = readConfig();
44
+
45
+ const url = new URL(path.startsWith("/") ? path : `/${path}`, `${baseUrl}/`);
46
+ if (options.query) {
47
+ for (const [key, value] of Object.entries(options.query)) {
48
+ if (value === undefined) continue;
49
+ if (Array.isArray(value)) {
50
+ if (value.length > 0) url.searchParams.set(key, value.join(","));
51
+ } else {
52
+ url.searchParams.set(key, String(value));
53
+ }
54
+ }
55
+ }
56
+
57
+ const headers: Record<string, string> = {
58
+ Accept: "application/json",
59
+ };
60
+ if (apiKey) {
61
+ headers.Authorization = `Bearer ${apiKey}`;
62
+ }
63
+
64
+ let body: BodyInit | undefined;
65
+ if (options.body !== undefined) {
66
+ headers["Content-Type"] = "application/json";
67
+ body = JSON.stringify(options.body);
68
+ }
69
+
70
+ const response = await fetch(url, { method, headers, body });
71
+
72
+ const contentType = response.headers.get("content-type") ?? "";
73
+ const isJson = contentType.includes("application/json");
74
+ const payload: unknown = isJson
75
+ ? await response.json().catch(() => null)
76
+ : await response.text().catch(() => null);
77
+
78
+ if (!response.ok) {
79
+ const message =
80
+ (isJson && payload && typeof payload === "object" && "error" in payload
81
+ ? String((payload as { error: unknown }).error)
82
+ : typeof payload === "string" && payload.length > 0
83
+ ? payload
84
+ : `HTTP ${response.status} ${response.statusText}`) ||
85
+ `HTTP ${response.status}`;
86
+ throw new HiaiDocsError(message, response.status, payload);
87
+ }
88
+
89
+ return payload as T;
90
90
  }
91
91
 
92
92
  function joinId(...segments: string[]): string {
93
- return segments
94
- .map((s) => encodeURIComponent(s))
95
- .join("/");
93
+ return segments.map((s) => encodeURIComponent(s)).join("/");
96
94
  }
97
95
 
98
96
  export const client = {
99
- search(params: {
100
- query: string;
101
- folder?: string;
102
- tags?: string[];
103
- limit?: number;
104
- }) {
105
- return request("GET", "/api/search", {
106
- query: {
107
- q: params.query,
108
- folder: params.folder,
109
- tags: params.tags,
110
- limit: params.limit,
111
- },
112
- });
113
- },
114
-
115
- getDocument(id: string) {
116
- return request("GET", `/api/${joinId("documents", id)}`);
117
- },
118
-
119
- createDocument(input: { title: string; content?: string; folderId?: string }) {
120
- return request("POST", "/api/documents", { body: input });
121
- },
122
-
123
- updateDocument(id: string, input: { title?: string; content?: string }) {
124
- return request("PATCH", `/api/${joinId("documents", id)}`, { body: input });
125
- },
126
-
127
- listDocuments(params: {
128
- folderId?: string;
129
- tag?: string;
130
- page?: number;
131
- limit?: number;
132
- }) {
133
- return request("GET", "/api/documents", { query: params });
134
- },
135
-
136
- listFolders(params: { parentId?: string }) {
137
- return request("GET", "/api/folders", { query: { parentId: params.parentId } });
138
- },
139
-
140
- createFolder(input: { name: string; parentId?: string }) {
141
- return request("POST", "/api/folders", { body: input });
142
- },
143
-
144
- createSnapshot(
145
- documentId: string,
146
- input: { label: string; description?: string },
147
- ) {
148
- return request(
149
- "POST",
150
- `/api/${joinId("documents", documentId, "versions")}`,
151
- { body: input },
152
- );
153
- },
154
-
155
- getVersionHistory(documentId: string, onlySnapshots?: boolean) {
156
- return request(
157
- "GET",
158
- `/api/${joinId("documents", documentId, "versions")}`,
159
- { query: { onlySnapshots } },
160
- );
161
- },
162
-
163
- async exportDocument(id: string): Promise<ExportResponse> {
164
- const markdown = await request<string>(
165
- "GET",
166
- `/api/${joinId("documents", id, "export")}`,
167
- );
168
- return { markdown };
169
- },
97
+ search(params: {
98
+ query: string;
99
+ folder?: string;
100
+ tags?: string[];
101
+ limit?: number;
102
+ }) {
103
+ return request("GET", "/api/search", {
104
+ query: {
105
+ q: params.query,
106
+ folder: params.folder,
107
+ tags: params.tags,
108
+ limit: params.limit,
109
+ },
110
+ });
111
+ },
112
+
113
+ getDocument(id: string) {
114
+ return request("GET", `/api/${joinId("documents", id)}`);
115
+ },
116
+
117
+ createDocument(input: {
118
+ title: string;
119
+ content?: string;
120
+ folderId?: string | null;
121
+ categoryId?: string | null;
122
+ }) {
123
+ return request("POST", "/api/documents", { body: input });
124
+ },
125
+
126
+ updateDocument(
127
+ id: string,
128
+ input: {
129
+ title?: string;
130
+ content?: string;
131
+ folderId?: string | null;
132
+ categoryId?: string | null;
133
+ },
134
+ ) {
135
+ return request("PATCH", `/api/${joinId("documents", id)}`, { body: input });
136
+ },
137
+
138
+ listDocuments(params: {
139
+ folderId?: string;
140
+ tag?: string;
141
+ page?: number;
142
+ limit?: number;
143
+ }) {
144
+ return request("GET", "/api/documents", { query: params });
145
+ },
146
+
147
+ listFolders(params: { parentId?: string }) {
148
+ return request("GET", "/api/folders", {
149
+ query: { parentId: params.parentId },
150
+ });
151
+ },
152
+
153
+ createFolder(input: {
154
+ name: string;
155
+ parentId?: string;
156
+ categoryId?: string;
157
+ }) {
158
+ return request("POST", "/api/folders", { body: input });
159
+ },
160
+
161
+ listCategories() {
162
+ return request("GET", "/api/categories");
163
+ },
164
+
165
+ createCategory(input: { name: string; description?: string }) {
166
+ return request("POST", "/api/categories", { body: input });
167
+ },
168
+
169
+ listTags() {
170
+ return request("GET", "/api/tags");
171
+ },
172
+
173
+ getRelatedDocuments(documentId: string, limit?: number) {
174
+ return request("GET", `/api/${joinId("graph", "related", documentId)}`, {
175
+ query: { limit },
176
+ });
177
+ },
178
+
179
+ searchGraph(input: { query: string; docIds: string[]; limit?: number }) {
180
+ return request("POST", "/api/graph/search", { body: input });
181
+ },
182
+
183
+ getDocumentIndexStatus(documentId: string) {
184
+ return request(
185
+ "GET",
186
+ `/api/${joinId("documents", documentId, "index-status")}`,
187
+ );
188
+ },
189
+
190
+ refreshDocumentIndex(documentId: string) {
191
+ return request(
192
+ "POST",
193
+ `/api/${joinId("documents", documentId, "index", "refresh")}`,
194
+ );
195
+ },
196
+
197
+ createSnapshot(
198
+ documentId: string,
199
+ input: { label: string; description?: string },
200
+ ) {
201
+ return request(
202
+ "POST",
203
+ `/api/${joinId("documents", documentId, "versions")}`,
204
+ { body: input },
205
+ );
206
+ },
207
+
208
+ getVersionHistory(documentId: string, onlySnapshots?: boolean) {
209
+ return request(
210
+ "GET",
211
+ `/api/${joinId("documents", documentId, "versions")}`,
212
+ { query: { onlySnapshots } },
213
+ );
214
+ },
215
+
216
+ async exportDocument(id: string): Promise<ExportResponse> {
217
+ const markdown = await request<string>(
218
+ "GET",
219
+ `/api/${joinId("documents", id, "export")}`,
220
+ );
221
+ return { markdown };
222
+ },
170
223
  };
171
224
 
172
225
  export type HiaiDocsClient = typeof client;
@@ -1,109 +1,8 @@
1
1
  #!/usr/bin/env bun
2
- /**
3
- * hiai-docs MCP Server
4
- *
5
- * Exposes hiai-docs operations as MCP tools via the stdio transport.
6
- *
7
- * Environment:
8
- * HIAI_DOCS_URL — base URL of the hiai-docs API
9
- * (default: http://localhost:50700)
10
- * HIAI_DOCS_API_KEY — bearer token used for Authorization header
11
- */
2
+ /** DocsMint MCP stdio entry point. */
12
3
 
13
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
- import { HiaiDocsError } from "./client.js";
16
- import type { ZodRawShape } from "zod";
4
+ import { serveStdio } from '@modelcontextprotocol/server/stdio';
17
5
 
18
- import * as search from "./tools/search.js";
19
- import * as getDocument from "./tools/get-document.js";
20
- import * as createDocument from "./tools/create-document.js";
21
- import * as updateDocument from "./tools/update-document.js";
22
- import * as listDocuments from "./tools/list-documents.js";
23
- import * as listFolders from "./tools/list-folders.js";
24
- import * as createFolder from "./tools/create-folder.js";
25
- import * as createSnapshot from "./tools/create-snapshot.js";
26
- import * as versionHistory from "./tools/version-history.js";
27
- import * as exportDocument from "./tools/export-document.js";
6
+ import { createDocsmintMcpServer } from './server.js';
28
7
 
29
- type ToolHandler = (args: Record<string, unknown>) => Promise<unknown>;
30
-
31
- interface McpToolResult {
32
- content: Array<{ type: "text"; text: string }>;
33
- isError?: boolean;
34
- }
35
-
36
- const server = new McpServer({
37
- name: "hiai-docs",
38
- version: "0.6.0",
39
- });
40
-
41
- /**
42
- * Wraps a tool handler to convert errors into MCP-formatted responses.
43
- * The SDK's tool callback returns CallToolResult; we return either success
44
- * with JSON-stringified content or an error with `isError: true`.
45
- */
46
- function wrapHandler(
47
- name: string,
48
- handler: ToolHandler,
49
- ): (args: Record<string, unknown>) => Promise<McpToolResult> {
50
- return async (args) => {
51
- try {
52
- const result = await handler(args);
53
- return {
54
- content: [
55
- {
56
- type: "text" as const,
57
- text: JSON.stringify(result, null, 2),
58
- },
59
- ],
60
- };
61
- } catch (err) {
62
- if (err instanceof HiaiDocsError) {
63
- return {
64
- isError: true,
65
- content: [
66
- {
67
- type: "text" as const,
68
- text: `hiai-docs API error (${err.status}): ${err.message}`,
69
- },
70
- ],
71
- };
72
- }
73
- const message = err instanceof Error ? err.message : String(err);
74
- return {
75
- isError: true,
76
- content: [
77
- {
78
- type: "text" as const,
79
- text: `Tool '${name}' failed: ${message}`,
80
- },
81
- ],
82
- };
83
- }
84
- };
85
- }
86
-
87
- function register<Args>(
88
- name: string,
89
- description: string,
90
- inputSchema: ZodRawShape,
91
- handler: (args: Args) => Promise<unknown>,
92
- ): void {
93
- server.tool(name, description, inputSchema as never, wrapHandler(name, handler as ToolHandler) as never);
94
- }
95
-
96
- register(search.definition.name, search.definition.description, search.definition.inputSchema as ZodRawShape, search.handler);
97
- register(getDocument.definition.name, getDocument.definition.description, getDocument.definition.inputSchema as ZodRawShape, getDocument.handler);
98
- register(createDocument.definition.name, createDocument.definition.description, createDocument.definition.inputSchema as ZodRawShape, createDocument.handler);
99
- register(updateDocument.definition.name, updateDocument.definition.description, updateDocument.definition.inputSchema as ZodRawShape, updateDocument.handler);
100
- register(listDocuments.definition.name, listDocuments.definition.description, listDocuments.definition.inputSchema as ZodRawShape, listDocuments.handler);
101
- register(listFolders.definition.name, listFolders.definition.description, listFolders.definition.inputSchema as ZodRawShape, listFolders.handler);
102
- register(createFolder.definition.name, createFolder.definition.description, createFolder.definition.inputSchema as ZodRawShape, createFolder.handler);
103
- register(createSnapshot.definition.name, createSnapshot.definition.description, createSnapshot.definition.inputSchema as ZodRawShape, createSnapshot.handler);
104
- register(versionHistory.definition.name, versionHistory.definition.description, versionHistory.definition.inputSchema as ZodRawShape, versionHistory.handler);
105
- register(exportDocument.definition.name, exportDocument.definition.description, exportDocument.definition.inputSchema as ZodRawShape, exportDocument.handler);
106
-
107
- const transport = new StdioServerTransport();
108
-
109
- await server.connect(transport);
8
+ serveStdio(() => createDocsmintMcpServer());
@@ -0,0 +1,87 @@
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
+ import { z, type ZodRawShape } from 'zod';
3
+
4
+ import { registerExtendedCapabilities } from './capabilities.js';
5
+ import { client as defaultClient, HiaiDocsError, type HiaiDocsClient } from './client.js';
6
+ import * as createDocument from './tools/create-document.js';
7
+ import * as createFolder from './tools/create-folder.js';
8
+ import * as createSnapshot from './tools/create-snapshot.js';
9
+ import * as exportDocument from './tools/export-document.js';
10
+ import * as getDocument from './tools/get-document.js';
11
+ import * as listDocuments from './tools/list-documents.js';
12
+ import * as listFolders from './tools/list-folders.js';
13
+ import * as search from './tools/search.js';
14
+ import * as updateDocument from './tools/update-document.js';
15
+ import * as versionHistory from './tools/version-history.js';
16
+
17
+ type ToolHandler = (args: Record<string, unknown>) => Promise<unknown>;
18
+ interface McpToolResult {
19
+ content: Array<{ type: 'text'; text: string }>;
20
+ isError?: boolean;
21
+ }
22
+
23
+ function wrapHandler(
24
+ name: string,
25
+ handler: ToolHandler
26
+ ): (args: Record<string, unknown>) => Promise<McpToolResult> {
27
+ return async (args) => {
28
+ try {
29
+ return {
30
+ content: [{ type: 'text', text: JSON.stringify(await handler(args), null, 2) }],
31
+ };
32
+ } catch (error) {
33
+ const message =
34
+ error instanceof HiaiDocsError
35
+ ? `DocsMint API error (${error.status}): ${error.message}`
36
+ : `Tool '${name}' failed: ${error instanceof Error ? error.message : String(error)}`;
37
+ return { isError: true, content: [{ type: 'text', text: message }] };
38
+ }
39
+ };
40
+ }
41
+
42
+ export function registerDocsmintMcpCapabilities(server: McpServer, client: HiaiDocsClient): void {
43
+ const register = <Args>(
44
+ name: string,
45
+ description: string,
46
+ inputSchema: ZodRawShape,
47
+ handler: (args: Args) => Promise<unknown>
48
+ ): void => {
49
+ server.registerTool(
50
+ name,
51
+ { description, inputSchema: z.object(inputSchema) },
52
+ wrapHandler(name, handler as ToolHandler) as never
53
+ );
54
+ };
55
+
56
+ const tools = [
57
+ search,
58
+ getDocument,
59
+ createDocument,
60
+ updateDocument,
61
+ listDocuments,
62
+ listFolders,
63
+ createFolder,
64
+ createSnapshot,
65
+ versionHistory,
66
+ exportDocument,
67
+ ] as const;
68
+ for (const tool of tools) {
69
+ register(
70
+ tool.definition.name,
71
+ tool.definition.description,
72
+ tool.definition.inputSchema as ZodRawShape,
73
+ tool.createHandler(client) as ToolHandler
74
+ );
75
+ }
76
+ registerExtendedCapabilities(server, client);
77
+ }
78
+
79
+ export interface CreateDocsmintMcpServerOptions {
80
+ client?: HiaiDocsClient;
81
+ }
82
+
83
+ export function createDocsmintMcpServer(options: CreateDocsmintMcpServerOptions = {}): McpServer {
84
+ const server = new McpServer({ name: 'docsmint', version: '0.6.2' });
85
+ registerDocsmintMcpCapabilities(server, options.client ?? defaultClient);
86
+ return server;
87
+ }
@@ -1,21 +1,21 @@
1
- import { z } from "zod";
2
- import { client } from "../client.js";
3
- import type { DocumentDetail } from "../types.js";
1
+ import { z } from 'zod';
2
+ import { client, type HiaiDocsClient } from '../client.js';
3
+ import type { DocumentDetail } from '../types.js';
4
4
 
5
5
  export const definition = {
6
- name: "create_document",
6
+ name: 'create_document',
7
7
  description:
8
- "Create a new document. Optionally provide initial markdown content and a folder ID.",
8
+ 'Create a new document. Optionally provide initial markdown content and a folder ID.',
9
9
  inputSchema: {
10
- title: z.string().describe("Document title."),
11
- content: z
10
+ title: z.string().describe('Document title.'),
11
+ content: z.string().optional().describe('Initial markdown content for the document.'),
12
+ folderId: z.string().optional().describe('Optional folder ID to place the document in.'),
13
+ categoryId: z
12
14
  .string()
13
15
  .optional()
14
- .describe("Initial markdown content for the document."),
15
- folderId: z
16
- .string()
17
- .optional()
18
- .describe("Optional folder ID to place the document in."),
16
+ .describe(
17
+ 'Optional category ID. Category keys are always rebound to their configured category.'
18
+ ),
19
19
  },
20
20
  } as const;
21
21
 
@@ -23,10 +23,12 @@ export interface CreateDocumentArgs {
23
23
  title: string;
24
24
  content?: string;
25
25
  folderId?: string;
26
+ categoryId?: string;
26
27
  }
27
28
 
28
- export async function handler(
29
- args: CreateDocumentArgs,
30
- ): Promise<DocumentDetail> {
31
- return (await client.createDocument(args)) as DocumentDetail;
32
- }
29
+ export const createHandler = (api: HiaiDocsClient) =>
30
+ async function createDocument(args: CreateDocumentArgs): Promise<DocumentDetail> {
31
+ return (await api.createDocument(args)) as DocumentDetail;
32
+ };
33
+
34
+ export const handler = createHandler(client);
@@ -1,24 +1,31 @@
1
- import { z } from "zod";
2
- import { client } from "../client.js";
3
- import type { Folder } from "../types.js";
1
+ import { z } from 'zod';
2
+ import { client, type HiaiDocsClient } from '../client.js';
3
+ import type { Folder } from '../types.js';
4
4
 
5
5
  export const definition = {
6
- name: "create_folder",
7
- description: "Create a new folder, optionally nested under a parent folder.",
6
+ name: 'create_folder',
7
+ description: 'Create a new folder, optionally nested under a parent folder.',
8
8
  inputSchema: {
9
- name: z.string().describe("Folder name."),
10
- parentId: z
9
+ name: z.string().describe('Folder name.'),
10
+ parentId: z.string().optional().describe('Optional parent folder ID for nesting.'),
11
+ categoryId: z
11
12
  .string()
12
13
  .optional()
13
- .describe("Optional parent folder ID for nesting."),
14
+ .describe(
15
+ 'Optional category ID. Category keys are always rebound to their configured category.'
16
+ ),
14
17
  },
15
18
  } as const;
16
19
 
17
20
  export interface CreateFolderArgs {
18
21
  name: string;
19
22
  parentId?: string;
23
+ categoryId?: string;
20
24
  }
21
25
 
22
- export async function handler(args: CreateFolderArgs): Promise<Folder> {
23
- return (await client.createFolder(args)) as Folder;
24
- }
26
+ export const createHandler = (api: HiaiDocsClient) =>
27
+ async function createFolder(args: CreateFolderArgs): Promise<Folder> {
28
+ return (await api.createFolder(args)) as Folder;
29
+ };
30
+
31
+ export const handler = createHandler(client);