@imenam/mcp-documentor 1.0.52 → 1.0.54

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.
@@ -90,7 +90,8 @@ export declare const FreshnessReportSchema: z.ZodObject<{
90
90
  export type FreshnessReport = z.infer<typeof FreshnessReportSchema>;
91
91
  export declare const CreateDocArgsSchema: z.ZodObject<{
92
92
  title: z.ZodString;
93
- content: z.ZodString;
93
+ content: z.ZodOptional<z.ZodString>;
94
+ content_file: z.ZodOptional<z.ZodString>;
94
95
  tracked_files: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
95
96
  path: z.ZodString;
96
97
  lines: z.ZodArray<z.ZodObject<{
@@ -106,6 +107,7 @@ export declare const UpdateDocArgsSchema: z.ZodObject<{
106
107
  path: z.ZodString;
107
108
  title: z.ZodOptional<z.ZodString>;
108
109
  content: z.ZodOptional<z.ZodString>;
110
+ content_file: z.ZodOptional<z.ZodString>;
109
111
  tracked_files: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
110
112
  path: z.ZodString;
111
113
  lines: z.ZodArray<z.ZodObject<{
@@ -140,8 +142,10 @@ export declare const GetDocMetadataArgsSchema: z.ZodObject<{
140
142
  }, z.core.$strip>;
141
143
  export declare const PatchDocArgsSchema: z.ZodObject<{
142
144
  path: z.ZodString;
143
- search: z.ZodString;
144
- replacement: z.ZodString;
145
+ search: z.ZodOptional<z.ZodString>;
146
+ search_file: z.ZodOptional<z.ZodString>;
147
+ replacement: z.ZodOptional<z.ZodString>;
148
+ replacement_file: z.ZodOptional<z.ZodString>;
145
149
  }, z.core.$strip>;
146
150
  export declare const CheckLinksArgsSchema: z.ZodObject<{
147
151
  project: z.ZodOptional<z.ZodString>;
@@ -45,16 +45,20 @@ export const FreshnessReportSchema = z.object({
45
45
  });
46
46
  export const CreateDocArgsSchema = z.object({
47
47
  title: z.string().describe("Title of the documentation entry"),
48
- content: z.string().describe("Documentation content in markdown"),
48
+ content: z.string().optional().describe("Documentation content in markdown"),
49
+ content_file: z.string().optional().describe("Absolute path to a text file whose content will be used instead of the content parameter. Use this to reuse a previously persisted draft."),
49
50
  tracked_files: z.array(TrackedFileInputSchema).optional().describe("Files to track for staleness detection. Each entry is either a plain path string (whole-file tracking) or an object { path, lines: [{ start, end }, ...] } for granular section tracking. Lines are 1-indexed and inclusive."),
50
51
  tags: z.array(z.string()).optional().describe("Tags for categorization"),
51
52
  folder: z.string().optional().describe("Folder path for organization, e.g. 'frontend/components'"),
52
53
  related_docs: z.array(z.string()).optional().describe("IDs of related documentation entries"),
54
+ }).refine(d => d.content || d.content_file, {
55
+ message: "Either content or content_file is required",
53
56
  });
54
57
  export const UpdateDocArgsSchema = z.object({
55
58
  path: z.string().describe("Path of the documentation entry to update, e.g. 'backend/auth/authentication' or 'authentication' for root docs"),
56
59
  title: z.string().optional().describe("Title of the documentation entry"),
57
60
  content: z.string().optional().describe("New content (replaces existing)"),
61
+ content_file: z.string().optional().describe("Absolute path to a text file whose content will be used instead of the content parameter. Use this to reuse a previously persisted draft."),
58
62
  tracked_files: z.array(TrackedFileInputSchema).optional().describe("New list of tracked files (replaces existing). Each entry is either a plain path string (whole-file tracking) or an object { path, lines: [{ start, end }, ...] } for granular section tracking. Lines are 1-indexed and inclusive."),
59
63
  tags: z.array(z.string()).optional().describe("New tags (replaces existing)"),
60
64
  related_docs: z.array(z.string()).optional().describe("Paths of related documentation entries (replaces existing)"),
@@ -83,8 +87,14 @@ export const GetDocMetadataArgsSchema = z.object({
83
87
  });
84
88
  export const PatchDocArgsSchema = z.object({
85
89
  path: z.string().describe("Path of the documentation entry to patch, e.g. 'backend/auth/authentication'"),
86
- search: z.string().describe("Exact text to find in the current content"),
87
- replacement: z.string().describe("Text to replace it with"),
90
+ search: z.string().optional().describe("Exact text to find in the current content"),
91
+ search_file: z.string().optional().describe("Absolute path to a text file whose content will be used as the search text. Use this to reuse a previously persisted draft."),
92
+ replacement: z.string().optional().describe("Text to replace it with"),
93
+ replacement_file: z.string().optional().describe("Absolute path to a text file whose content will be used as the replacement. Use this to reuse a previously persisted draft."),
94
+ }).refine(d => d.search || d.search_file, {
95
+ message: "Either search or search_file is required",
96
+ }).refine(d => d.replacement || d.replacement_file, {
97
+ message: "Either replacement or replacement_file is required",
88
98
  });
89
99
  export const CheckLinksArgsSchema = z.object({
90
100
  project: z.string().optional().describe("Project to check (defaults to current project)"),
@@ -0,0 +1,2 @@
1
+ export declare function persistDraft(dataDir: string, label: string, content: string): string;
2
+ export declare function readDraft(filePath: string): string;
@@ -0,0 +1,14 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ export function persistDraft(dataDir, label, content) {
4
+ const draftsDir = path.join(dataDir, "errors-tmp-files");
5
+ fs.mkdirSync(draftsDir, { recursive: true });
6
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
7
+ const filename = `${timestamp}-${label}.txt`;
8
+ const filePath = path.join(draftsDir, filename);
9
+ fs.writeFileSync(filePath, content, "utf-8");
10
+ return filePath;
11
+ }
12
+ export function readDraft(filePath) {
13
+ return fs.readFileSync(filePath, "utf-8");
14
+ }
package/dist/src/index.js CHANGED
@@ -38,6 +38,7 @@ import { reindexEmbeddings } from "./tools/reindex-embeddings.js";
38
38
  import { prepareExportBundle } from "./tools/prepare-export-bundle.js";
39
39
  import { deleteExportBundle } from "./tools/delete-export-bundle.js";
40
40
  import { uploadImage } from "./tools/upload-image.js";
41
+ import { getImage } from "./tools/get-image.js";
41
42
  const __filename = fileURLToPath(import.meta.url);
42
43
  const __dirname = path.dirname(__filename);
43
44
  function findProjectRoot(startDir) {
@@ -620,6 +621,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
620
621
  properties: {
621
622
  title: { type: "string", description: "Title of the documentation entry" },
622
623
  content: { type: "string", description: "Documentation content in markdown. To link to another document inline, use the syntax [text](doc://target-doc-id) — the viewer will navigate to that document on click. To link to a heading within the same document, use [text](#heading-slug). Use check_links to validate both types. For code blocks, always specify a language identifier for syntax highlighting, e.g. ```typescript, ```python, ```bash, ```json, ```yaml, ```sql." },
624
+ content_file: { type: "string", description: "Absolute path to a text file whose content will be used instead of the content parameter. Use this to reuse a previously persisted draft (path is returned in the error message when a previous call failed)." },
623
625
  tracked_files: {
624
626
  type: "array",
625
627
  items: {
@@ -663,7 +665,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
663
665
  },
664
666
  project: { type: "string", description: "Project to create the doc in (defaults to current project if omitted). If the project name does not exist yet, it will be created automatically." },
665
667
  },
666
- required: ["title", "content"],
668
+ required: ["title"],
667
669
  },
668
670
  },
669
671
  {
@@ -699,6 +701,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
699
701
  description: "New display title stored in frontmatter. Does not rename the file or change the document path — use move_doc to relocate or rename on disk.",
700
702
  },
701
703
  content: { type: "string", description: "New content (replaces existing). Supports [text](doc://target-doc-path) to link inline to other documents, and [text](#heading-slug) to link to headings within this document. For code blocks, always specify a language identifier for syntax highlighting, e.g. ```typescript, ```python, ```bash, ```json, ```yaml, ```sql." },
704
+ content_file: { type: "string", description: "Absolute path to a text file whose content will be used instead of the content parameter. Use this to reuse a previously persisted draft (path is returned in the error message when a previous call failed)." },
702
705
  tracked_files: {
703
706
  type: "array",
704
707
  items: {
@@ -837,9 +840,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
837
840
  properties: {
838
841
  path: { type: "string", description: "Path of the documentation entry to patch, e.g. 'backend/auth/authentication'" },
839
842
  search: { type: "string", description: "Exact text to find in the current content" },
843
+ search_file: { type: "string", description: "Absolute path to a text file whose content will be used as the search text. Use this to reuse a previously persisted draft (path is returned in the error message when a previous call failed)." },
840
844
  replacement: { type: "string", description: "Text to replace it with" },
845
+ replacement_file: { type: "string", description: "Absolute path to a text file whose content will be used as the replacement. Use this to reuse a previously persisted draft (path is returned in the error message when a previous call failed)." },
841
846
  },
842
- required: ["path", "search", "replacement"],
847
+ required: ["path", "search"],
843
848
  },
844
849
  },
845
850
  {
@@ -987,6 +992,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
987
992
  required: ["filename", "filePath"],
988
993
  },
989
994
  },
995
+ {
996
+ name: "get_image",
997
+ description: "Retrieve an image stored in a project's assets by its URI and return it as a base64-encoded image. Use this when a document references an image (e.g. ![alt](/api/images/{project}/{filename})) and you need to view its content.",
998
+ inputSchema: {
999
+ type: "object",
1000
+ properties: {
1001
+ uri: { type: "string", description: "Image URI as it appears in the document, e.g. '/api/images/my-project/screenshot.png'" },
1002
+ },
1003
+ required: ["uri"],
1004
+ },
1005
+ },
990
1006
  ],
991
1007
  };
992
1008
  });
@@ -1058,6 +1074,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1058
1074
  }
1059
1075
  case "upload_image":
1060
1076
  return await uploadImage(storageEngine, args);
1077
+ case "get_image":
1078
+ return await getImage(storageEngine, args);
1061
1079
  default:
1062
1080
  throw new Error(`Tool not found: ${name}`);
1063
1081
  }
@@ -3,6 +3,7 @@ import path from "path";
3
3
  import { CreateDocArgsSchema } from "../../shared/schemas/doc.schema.js";
4
4
  import { computeFileHash } from "./check-freshness.js";
5
5
  import { snapshotSection } from "../core/section-tracker.js";
6
+ import { persistDraft } from "../core/draft-store.js";
6
7
  export function slugify(title) {
7
8
  return title
8
9
  .normalize("NFD")
@@ -36,6 +37,9 @@ export async function createDoc(storageEngine, embeddingEngine, args) {
36
37
  try {
37
38
  const validated = CreateDocArgsSchema.parse(args);
38
39
  const { project } = (args ?? {});
40
+ const content = validated.content_file
41
+ ? fs.readFileSync(validated.content_file, "utf-8")
42
+ : validated.content;
39
43
  const slug = slugify(validated.title);
40
44
  const id = validated.folder ? `${validated.folder}/${slug}` : slug;
41
45
  const now = new Date().toISOString();
@@ -54,7 +58,7 @@ export async function createDoc(storageEngine, embeddingEngine, args) {
54
58
  const doc = {
55
59
  id,
56
60
  title: validated.title,
57
- content: validated.content,
61
+ content,
58
62
  tracked_files: trackedFiles,
59
63
  tags: validated.tags ?? [],
60
64
  folder,
@@ -65,11 +69,14 @@ export async function createDoc(storageEngine, embeddingEngine, args) {
65
69
  const resolvedProject = project ?? storageEngine.getConfig().currentProject;
66
70
  await storageEngine.saveDoc(doc, resolvedProject, "create");
67
71
  if (embeddingEngine.isConfigured()) {
68
- const embedding = await embeddingEngine.generateEmbedding(validated.content);
72
+ const embedding = await embeddingEngine.generateEmbedding(content);
69
73
  if (embedding) {
70
74
  await embeddingEngine.saveEmbedding(id, embedding, resolvedProject);
71
75
  }
72
76
  }
77
+ if (validated.content_file) {
78
+ fs.unlinkSync(validated.content_file);
79
+ }
73
80
  return {
74
81
  content: [
75
82
  {
@@ -81,6 +88,14 @@ export async function createDoc(storageEngine, embeddingEngine, args) {
81
88
  }
82
89
  catch (err) {
83
90
  const message = err instanceof Error ? err.message : String(err);
91
+ const rawContent = args?.content;
92
+ if (rawContent) {
93
+ const draftPath = persistDraft(storageEngine.getDataDir(), "create-doc", rawContent);
94
+ return {
95
+ isError: true,
96
+ content: [{ type: "text", text: `Error creating doc: ${message}\n\n⚠️ The content has been saved to: ${draftPath}\nYou can reuse it directly via the content_file parameter instead of rewriting it.` }],
97
+ };
98
+ }
84
99
  return { isError: true, content: [{ type: "text", text: `Error creating doc: ${message}` }] };
85
100
  }
86
101
  }
@@ -0,0 +1,15 @@
1
+ import type { StorageEngine } from "../core/storage-engine.js";
2
+ export declare function getImage(storageEngine: StorageEngine, args: unknown): Promise<{
3
+ isError: boolean;
4
+ content: {
5
+ type: string;
6
+ text: string;
7
+ }[];
8
+ } | {
9
+ content: {
10
+ type: string;
11
+ data: string;
12
+ mimeType: string;
13
+ }[];
14
+ isError?: undefined;
15
+ }>;
@@ -0,0 +1,67 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { z } from "zod";
4
+ const GetImageArgsSchema = z.object({
5
+ uri: z.string().min(1),
6
+ });
7
+ const MIME_TYPES = {
8
+ ".png": "image/png",
9
+ ".jpg": "image/jpeg",
10
+ ".jpeg": "image/jpeg",
11
+ ".gif": "image/gif",
12
+ ".webp": "image/webp",
13
+ ".svg": "image/svg+xml",
14
+ ".bmp": "image/bmp",
15
+ ".ico": "image/x-icon",
16
+ };
17
+ export async function getImage(storageEngine, args) {
18
+ try {
19
+ const validated = GetImageArgsSchema.parse(args);
20
+ // Expected URI format: /api/images/{project}/{filename}
21
+ const match = validated.uri.match(/^\/api\/images\/([^/]+)\/([^/]+)$/);
22
+ if (!match) {
23
+ return {
24
+ isError: true,
25
+ content: [
26
+ {
27
+ type: "text",
28
+ text: `Invalid image URI format. Expected: /api/images/{project}/{filename}. Got: ${validated.uri}`,
29
+ },
30
+ ],
31
+ };
32
+ }
33
+ const project = decodeURIComponent(match[1]);
34
+ const filename = decodeURIComponent(match[2]);
35
+ const assetsDir = path.join(storageEngine.getDataDir(), "projects", project, "assets");
36
+ const filePath = path.join(assetsDir, filename);
37
+ // Prevent path traversal
38
+ if (!filePath.startsWith(assetsDir)) {
39
+ return {
40
+ isError: true,
41
+ content: [{ type: "text", text: "Invalid image path." }],
42
+ };
43
+ }
44
+ if (!fs.existsSync(filePath)) {
45
+ return {
46
+ isError: true,
47
+ content: [{ type: "text", text: `Image not found: ${validated.uri}` }],
48
+ };
49
+ }
50
+ const ext = path.extname(filename).toLowerCase();
51
+ const mimeType = MIME_TYPES[ext] ?? "application/octet-stream";
52
+ const data = fs.readFileSync(filePath).toString("base64");
53
+ return {
54
+ content: [
55
+ {
56
+ type: "image",
57
+ data,
58
+ mimeType,
59
+ },
60
+ ],
61
+ };
62
+ }
63
+ catch (err) {
64
+ const message = err instanceof Error ? err.message : String(err);
65
+ return { isError: true, content: [{ type: "text", text: `Error retrieving image: ${message}` }] };
66
+ }
67
+ }
@@ -1,4 +1,6 @@
1
+ import fs from "fs";
1
2
  import { PatchDocArgsSchema } from "../../shared/schemas/doc.schema.js";
3
+ import { persistDraft } from "../core/draft-store.js";
2
4
  export async function patchDoc(storageEngine, embeddingEngine, args) {
3
5
  try {
4
6
  const validated = PatchDocArgsSchema.parse(args);
@@ -7,7 +9,13 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
7
9
  return { isError: true, content: [{ type: "text", text: `Documentation entry "${validated.path}" not found in any project.` }] };
8
10
  }
9
11
  const { doc, project: resolvedProject } = resolved;
10
- if (!doc.content.includes(validated.search)) {
12
+ const search = validated.search_file
13
+ ? fs.readFileSync(validated.search_file, "utf-8")
14
+ : validated.search;
15
+ const replacement = validated.replacement_file
16
+ ? fs.readFileSync(validated.replacement_file, "utf-8")
17
+ : validated.replacement;
18
+ if (!doc.content.includes(search)) {
11
19
  const preview = doc.content.slice(0, 300).replace(/\n/g, "↵");
12
20
  return {
13
21
  isError: true,
@@ -17,7 +25,7 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
17
25
  }],
18
26
  };
19
27
  }
20
- doc.content = doc.content.replace(validated.search, validated.replacement);
28
+ doc.content = doc.content.replace(search, replacement);
21
29
  doc.updatedAt = new Date().toISOString();
22
30
  await storageEngine.saveDoc(doc, resolvedProject, "patch");
23
31
  if (embeddingEngine.isConfigured()) {
@@ -26,12 +34,33 @@ export async function patchDoc(storageEngine, embeddingEngine, args) {
26
34
  await embeddingEngine.saveEmbedding(validated.path, embedding, resolvedProject);
27
35
  }
28
36
  }
37
+ if (validated.search_file)
38
+ fs.unlinkSync(validated.search_file);
39
+ if (validated.replacement_file)
40
+ fs.unlinkSync(validated.replacement_file);
29
41
  return {
30
42
  content: [{ type: "text", text: `Documentation entry "${validated.path}" patched successfully.` }],
31
43
  };
32
44
  }
33
45
  catch (err) {
34
46
  const message = err instanceof Error ? err.message : String(err);
47
+ const rawSearch = args?.search;
48
+ const rawReplacement = args?.replacement;
49
+ if (rawSearch || rawReplacement) {
50
+ const parts = [];
51
+ if (rawSearch) {
52
+ const searchPath = persistDraft(storageEngine.getDataDir(), "patch-doc-search", rawSearch);
53
+ parts.push(`search → ${searchPath}`);
54
+ }
55
+ if (rawReplacement) {
56
+ const replacementPath = persistDraft(storageEngine.getDataDir(), "patch-doc-replacement", rawReplacement);
57
+ parts.push(`replacement → ${replacementPath}`);
58
+ }
59
+ return {
60
+ isError: true,
61
+ content: [{ type: "text", text: `Error patching doc: ${message}\n\n⚠️ The following content has been saved:\n${parts.join("\n")}\nYou can reuse them directly via the search_file / replacement_file parameters instead of rewriting them.` }],
62
+ };
63
+ }
35
64
  return { isError: true, content: [{ type: "text", text: `Error patching doc: ${message}` }] };
36
65
  }
37
66
  }
@@ -3,6 +3,7 @@ import path from "path";
3
3
  import { UpdateDocArgsSchema } from "../../shared/schemas/doc.schema.js";
4
4
  import { computeFileHash } from "./check-freshness.js";
5
5
  import { snapshotSection } from "../core/section-tracker.js";
6
+ import { persistDraft } from "../core/draft-store.js";
6
7
  function snapshotTrackedFiles(inputs) {
7
8
  const tracked = [];
8
9
  for (const input of inputs) {
@@ -35,7 +36,10 @@ export async function updateDoc(storageEngine, embeddingEngine, args) {
35
36
  if (validated.title !== undefined) {
36
37
  doc.title = validated.title;
37
38
  }
38
- if (validated.content !== undefined) {
39
+ if (validated.content_file !== undefined) {
40
+ doc.content = fs.readFileSync(validated.content_file, "utf-8");
41
+ }
42
+ else if (validated.content !== undefined) {
39
43
  doc.content = validated.content;
40
44
  }
41
45
  if (validated.tracked_files !== undefined) {
@@ -49,17 +53,22 @@ export async function updateDoc(storageEngine, embeddingEngine, args) {
49
53
  }
50
54
  doc.updatedAt = new Date().toISOString();
51
55
  await storageEngine.saveDoc(doc, resolvedProject, "update");
52
- if (validated.content !== undefined && embeddingEngine.isConfigured()) {
53
- const embedding = await embeddingEngine.generateEmbedding(validated.content);
56
+ if ((validated.content !== undefined || validated.content_file !== undefined) && embeddingEngine.isConfigured()) {
57
+ const embedding = await embeddingEngine.generateEmbedding(doc.content);
54
58
  if (embedding) {
55
59
  await embeddingEngine.saveEmbedding(validated.path, embedding, resolvedProject);
56
60
  }
57
61
  }
62
+ if (validated.content_file !== undefined) {
63
+ fs.unlinkSync(validated.content_file);
64
+ }
58
65
  const changes = [];
59
66
  if (validated.title !== undefined)
60
67
  changes.push(`title (${doc.title})`);
61
68
  if (validated.content !== undefined)
62
69
  changes.push("content");
70
+ if (validated.content_file !== undefined)
71
+ changes.push("content (from file)");
63
72
  if (validated.tracked_files !== undefined)
64
73
  changes.push(`tracked_files (${doc.tracked_files.length} files)`);
65
74
  if (validated.tags !== undefined)
@@ -74,6 +83,14 @@ export async function updateDoc(storageEngine, embeddingEngine, args) {
74
83
  }
75
84
  catch (err) {
76
85
  const message = err instanceof Error ? err.message : String(err);
86
+ const rawContent = args?.content;
87
+ if (rawContent) {
88
+ const draftPath = persistDraft(storageEngine.getDataDir(), "update-doc", rawContent);
89
+ return {
90
+ isError: true,
91
+ content: [{ type: "text", text: `Error updating doc: ${message}\n\n⚠️ The content has been saved to: ${draftPath}\nYou can reuse it directly via the content_file parameter instead of rewriting it.` }],
92
+ };
93
+ }
77
94
  return { isError: true, content: [{ type: "text", text: `Error updating doc: ${message}` }] };
78
95
  }
79
96
  }
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.52";
1
+ export declare const APP_VERSION = "1.0.54";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated version file - do not edit manually
2
- export const APP_VERSION = '1.0.52';
2
+ export const APP_VERSION = '1.0.54';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-documentor",
3
- "version": "1.0.52",
3
+ "version": "1.0.54",
4
4
  "main": "dist/src/index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",