@doquflow/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.
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extract = extract;
4
+ function uniq(arr) {
5
+ return Array.from(new Set(arr.filter((s) => s && s.length > 0)));
6
+ }
7
+ function collect(re, content, group = 1) {
8
+ const out = [];
9
+ let m;
10
+ while ((m = re.exec(content)) !== null) {
11
+ if (m[group])
12
+ out.push(m[group]);
13
+ }
14
+ return out;
15
+ }
16
+ const RE_CLASS = /(?:abstract\s+|sealed\s+|partial\s+|public\s+|private\s+|protected\s+|internal\s+|static\s+)*(?:class|interface|struct|enum|record)\s+([A-Z]\w*)/g;
17
+ const RE_FUNC_KW = /(?:public|private|protected|internal|static|async|override|function|def|fn|sub)\s+(?:[\w<>\[\],?\s]+?\s+)?([A-Za-z_]\w*)\s*\(/g;
18
+ const RE_FUNC_ARROW = /\b([A-Za-z_]\w*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g;
19
+ const RE_METHOD_TS = /^\s*(?:public|private|protected|static|async|readonly|\s)*([a-z_]\w*)\s*\([^)]*\)\s*[:{]/gm;
20
+ const RE_USING = /\busing\s+([\w.]+)\s*;/g;
21
+ const RE_IMPORT_FROM = /\bimport\s+(?:[\w*${}\s,]+\s+from\s+)?['"]([^'"]+)['"]/g;
22
+ const RE_REQUIRE = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g;
23
+ const RE_DECORATOR = /@(Injectable|Component|Inject|Service|Module|Directive|Pipe|NgModule|Controller)\b/g;
24
+ const RE_NEW_CLASS = /\bnew\s+([A-Z]\w*)\s*\(/g;
25
+ // Standard SQL FROM/JOIN/INTO/UPDATE/DELETE patterns.
26
+ const RE_SQL_TABLE = /(?:\bFROM|\bJOIN|\bINTO|\bUPDATE|\bDELETE\s+FROM|\bTABLE)\s+([A-Za-z_][\w.]*)/gi;
27
+ // EF / ORM DbSet<T> declaration.
28
+ const RE_DBSET = /\bDbSet<\s*([A-Z]\w*)\s*>/g;
29
+ // Attribute-based table mapping: [Table("name")] and .Table("name")
30
+ const RE_TABLE_ATTR = /\[Table\(\s*["']([^"']+)["']/g;
31
+ const RE_TABLE_FLUENT = /\.Table\(\s*["']([^"']+)["']\s*\)/g;
32
+ // EF DbContext property access: _db.Orders., context.Users., _repository.Items. etc.
33
+ // Catches patterns like `_db.Orders.ToListAsync()`, `_context.Users.Find(id)`.
34
+ const RE_EF_PROP = /\b(?:_?db|_?context|_?ctx|_?repository|_?repo)\s*\.\s*([A-Z]\w+)(?=[\s.(])/g;
35
+ const RE_DOTNET_ROUTE = /\[(?:Route|Http(?:Get|Post|Put|Delete|Patch))\(\s*["']([^"']*)["']/g;
36
+ const RE_MIN_API = /\bapp\.Map(?:Get|Post|Put|Delete|Patch)\s*\(\s*["']([^"']+)["']/g;
37
+ const RE_EXPRESS = /\b(?:router|app)\.(?:get|post|put|delete|patch|use)\s*\(\s*['"]([^'"]+)['"]/g;
38
+ const RE_NEST = /@(?:Get|Post|Put|Delete|Patch)\s*\(\s*['"]([^'"]+)['"]/g;
39
+ const RE_NG_PATH = /\bpath\s*:\s*['"]([^'"]+)['"]/g;
40
+ const RE_CFG_CONNSTR = /\bConnectionStrings:([\w.]+)/g;
41
+ const RE_PROCESS_ENV = /\bprocess\.env\.([A-Z_][A-Z0-9_]*)/g;
42
+ const RE_DOTNET_ENV = /\bEnvironment\.GetEnvironmentVariable\(\s*["']([^"']+)["']/g;
43
+ const RE_ICONFIG = /\bIConfiguration\b/g;
44
+ const RE_APPSETTINGS = /\bappsettings(?:\.[A-Za-z0-9]+)?\.json\b/g;
45
+ // SQL keywords and noise that the regex may pick up as table names.
46
+ // Also strips single-letter hits (LINQ aliases: `from u in _db.Users` → u).
47
+ const SQL_KEYWORD_NOISE = new Set([
48
+ "select", "where", "set", "on", "as", "by", "in", "is", "not", "null",
49
+ "and", "or", "top", "all", "any", "exists", "between", "like", "case",
50
+ "when", "then", "else", "end", "with", "having", "group", "order",
51
+ "asc", "desc", "distinct", "outer", "inner", "left", "right", "full",
52
+ "cross", "natural", "values", "default", "primary", "key", "index",
53
+ ]);
54
+ // EF DbContext property names that are framework plumbing, not tables.
55
+ const EF_NOISE = new Set([
56
+ "SaveChanges", "SaveChangesAsync", "Entry", "Add", "AddRange", "Remove",
57
+ "RemoveRange", "Update", "UpdateRange", "Attach", "Find", "FindAsync",
58
+ "FromSqlRaw", "FromSqlInterpolated", "Database", "Model", "ChangeTracker",
59
+ "Configuration", "ToList", "ToListAsync", "FirstOrDefault", "FirstOrDefaultAsync",
60
+ "Where", "Any", "Count", "Include", "ThenInclude", "OrderBy", "GroupBy", "Select",
61
+ ]);
62
+ function cleanTableNames(names) {
63
+ return names.filter((n) => {
64
+ if (n.length < 2)
65
+ return false;
66
+ if (SQL_KEYWORD_NOISE.has(n.toLowerCase()))
67
+ return false;
68
+ if (EF_NOISE.has(n))
69
+ return false;
70
+ return true;
71
+ });
72
+ }
73
+ function extract(content) {
74
+ const classes = collect(new RegExp(RE_CLASS.source, "g"), content);
75
+ const functions = uniq([
76
+ ...collect(new RegExp(RE_FUNC_KW.source, "g"), content),
77
+ ...collect(new RegExp(RE_FUNC_ARROW.source, "g"), content),
78
+ ...collect(new RegExp(RE_METHOD_TS.source, "gm"), content),
79
+ ]).filter((n) => !["if", "for", "while", "switch", "catch", "return", "function"].includes(n));
80
+ const dependencies = uniq([
81
+ ...collect(new RegExp(RE_USING.source, "g"), content),
82
+ ...collect(new RegExp(RE_IMPORT_FROM.source, "g"), content),
83
+ ...collect(new RegExp(RE_REQUIRE.source, "g"), content),
84
+ ...collect(new RegExp(RE_DECORATOR.source, "g"), content),
85
+ ...collect(new RegExp(RE_NEW_CLASS.source, "g"), content),
86
+ ]);
87
+ const db_tables = uniq(cleanTableNames([
88
+ ...collect(new RegExp(RE_SQL_TABLE.source, "gi"), content),
89
+ ...collect(new RegExp(RE_DBSET.source, "g"), content),
90
+ ...collect(new RegExp(RE_TABLE_ATTR.source, "g"), content),
91
+ ...collect(new RegExp(RE_TABLE_FLUENT.source, "g"), content),
92
+ ...collect(new RegExp(RE_EF_PROP.source, "g"), content),
93
+ ]));
94
+ const endpoints = uniq([
95
+ ...collect(new RegExp(RE_DOTNET_ROUTE.source, "g"), content),
96
+ ...collect(new RegExp(RE_MIN_API.source, "g"), content),
97
+ ...collect(new RegExp(RE_EXPRESS.source, "g"), content),
98
+ ...collect(new RegExp(RE_NEST.source, "g"), content),
99
+ ...collect(new RegExp(RE_NG_PATH.source, "g"), content),
100
+ ]);
101
+ const config_refs = [];
102
+ config_refs.push(...collect(new RegExp(RE_CFG_CONNSTR.source, "g"), content).map((s) => `ConnectionStrings:${s}`));
103
+ config_refs.push(...collect(new RegExp(RE_PROCESS_ENV.source, "g"), content).map((s) => `process.env.${s}`));
104
+ config_refs.push(...collect(new RegExp(RE_DOTNET_ENV.source, "g"), content));
105
+ if (RE_ICONFIG.test(content))
106
+ config_refs.push("IConfiguration");
107
+ if (RE_APPSETTINGS.test(content))
108
+ config_refs.push("appsettings.json");
109
+ return {
110
+ classes: uniq(classes),
111
+ functions: uniq(functions),
112
+ dependencies,
113
+ db_tables,
114
+ endpoints,
115
+ config_refs: uniq(config_refs),
116
+ };
117
+ }
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.walk = walk;
7
+ exports.isBinary = isBinary;
8
+ exports.safeReadFile = safeReadFile;
9
+ exports.ensureDir = ensureDir;
10
+ exports.writeFileAtomic = writeFileAtomic;
11
+ exports.readJsonIfExists = readJsonIfExists;
12
+ const promises_1 = __importDefault(require("node:fs/promises"));
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const SKIP_DIRS = new Set([
15
+ "node_modules",
16
+ "vendor",
17
+ ".git",
18
+ ".svn",
19
+ ".hg",
20
+ "dist",
21
+ "build",
22
+ "bin",
23
+ "obj",
24
+ ".docuflow",
25
+ ".next",
26
+ ".nuxt",
27
+ "out",
28
+ "target",
29
+ ".venv",
30
+ "venv",
31
+ "__pycache__",
32
+ ]);
33
+ const SKIP_FILE_PATTERNS = [
34
+ /\.min\.js$/i,
35
+ /\.min\.css$/i,
36
+ /\.map$/i,
37
+ /\.lock$/i,
38
+ /package-lock\.json$/i,
39
+ ];
40
+ const MAX_FILE_BYTES = 300 * 1024;
41
+ async function walk(root) {
42
+ const files = [];
43
+ const skipped = [];
44
+ const absRoot = node_path_1.default.resolve(root);
45
+ async function recurse(dir) {
46
+ let entries;
47
+ try {
48
+ entries = await promises_1.default.readdir(dir, { withFileTypes: true });
49
+ }
50
+ catch (e) {
51
+ skipped.push({ path: dir, reason: `readdir failed: ${e.message}` });
52
+ return;
53
+ }
54
+ for (const entry of entries) {
55
+ const full = node_path_1.default.join(dir, entry.name);
56
+ if (entry.isDirectory()) {
57
+ if (SKIP_DIRS.has(entry.name))
58
+ continue;
59
+ await recurse(full);
60
+ }
61
+ else if (entry.isFile()) {
62
+ if (SKIP_FILE_PATTERNS.some((re) => re.test(entry.name))) {
63
+ skipped.push({ path: full, reason: "skipped by pattern" });
64
+ continue;
65
+ }
66
+ let stat;
67
+ try {
68
+ stat = await promises_1.default.stat(full);
69
+ }
70
+ catch (e) {
71
+ skipped.push({ path: full, reason: `stat failed: ${e.message}` });
72
+ continue;
73
+ }
74
+ if (stat.size > MAX_FILE_BYTES) {
75
+ skipped.push({ path: full, reason: `file >300KB (${stat.size} bytes)` });
76
+ continue;
77
+ }
78
+ files.push({ path: full, size: stat.size });
79
+ }
80
+ }
81
+ }
82
+ try {
83
+ const stat = await promises_1.default.stat(absRoot);
84
+ if (!stat.isDirectory()) {
85
+ skipped.push({ path: absRoot, reason: "not a directory" });
86
+ return { files, skipped };
87
+ }
88
+ }
89
+ catch (e) {
90
+ skipped.push({ path: absRoot, reason: `root stat failed: ${e.message}` });
91
+ return { files, skipped };
92
+ }
93
+ await recurse(absRoot);
94
+ return { files, skipped };
95
+ }
96
+ function isBinary(buf) {
97
+ const len = Math.min(buf.length, 512);
98
+ for (let i = 0; i < len; i++) {
99
+ if (buf[i] === 0)
100
+ return true;
101
+ }
102
+ return false;
103
+ }
104
+ async function safeReadFile(filePath) {
105
+ try {
106
+ const buf = await promises_1.default.readFile(filePath);
107
+ if (isBinary(buf)) {
108
+ return { size: buf.length, binary: true };
109
+ }
110
+ return { content: buf.toString("utf8"), size: buf.length };
111
+ }
112
+ catch (e) {
113
+ return { size: 0, error: e.message };
114
+ }
115
+ }
116
+ async function ensureDir(dir) {
117
+ await promises_1.default.mkdir(dir, { recursive: true });
118
+ }
119
+ async function writeFileAtomic(filePath, content) {
120
+ await ensureDir(node_path_1.default.dirname(filePath));
121
+ await promises_1.default.writeFile(filePath, content, "utf8");
122
+ return Buffer.byteLength(content, "utf8");
123
+ }
124
+ async function readJsonIfExists(filePath) {
125
+ try {
126
+ const txt = await promises_1.default.readFile(filePath, "utf8");
127
+ return JSON.parse(txt);
128
+ }
129
+ catch {
130
+ return null;
131
+ }
132
+ }
package/dist/index.js ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const read_module_1 = require("./tools/read-module");
8
+ const list_modules_1 = require("./tools/list-modules");
9
+ const write_spec_1 = require("./tools/write-spec");
10
+ const read_specs_1 = require("./tools/read-specs");
11
+ const server = new index_js_1.Server({ name: "docuflow", version: "0.1.0" }, { capabilities: { tools: {} } });
12
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
13
+ tools: [
14
+ {
15
+ name: "read_module",
16
+ description: "Read a single source file, detect its language, and extract classes, functions, dependencies, DB tables, endpoints, and config references. Returns raw content truncated at 8000 chars.",
17
+ inputSchema: {
18
+ type: "object",
19
+ properties: {
20
+ path: { type: "string", description: "Absolute or relative path to the source file." },
21
+ },
22
+ required: ["path"],
23
+ },
24
+ },
25
+ {
26
+ name: "list_modules",
27
+ description: "Walk a project directory and return extracted facts for every non-binary file. Skips node_modules, dist, build, .git, vendor, obj, bin, .docuflow, *.min.js, *.map, *.lock, and files >300KB. Raw content is omitted for bulk results.",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {
31
+ path: { type: "string", description: "Root directory to scan." },
32
+ extensions: {
33
+ type: "array",
34
+ items: { type: "string" },
35
+ description: "Optional extension filter e.g. [\".cs\",\".ts\"]. If omitted all non-binary files are included.",
36
+ },
37
+ },
38
+ required: ["path"],
39
+ },
40
+ },
41
+ {
42
+ name: "write_spec",
43
+ description: "Write a markdown spec file to <project_path>/.docuflow/specs/<filename>.md and update the index. The agent provides the full markdown content.",
44
+ inputSchema: {
45
+ type: "object",
46
+ properties: {
47
+ project_path: { type: "string", description: "Root of the project (where .docuflow/ will be created)." },
48
+ filename: { type: "string", description: "Name for the spec file, without extension." },
49
+ content: { type: "string", description: "Full markdown content to write." },
50
+ },
51
+ required: ["project_path", "filename", "content"],
52
+ },
53
+ },
54
+ {
55
+ name: "read_specs",
56
+ description: "Read previously written specs from <project_path>/.docuflow/specs/. Optionally filter by module name.",
57
+ inputSchema: {
58
+ type: "object",
59
+ properties: {
60
+ project_path: { type: "string", description: "Root of the project." },
61
+ module_name: {
62
+ type: "string",
63
+ description: "Optional: name of a specific spec to retrieve (with or without .md).",
64
+ },
65
+ },
66
+ required: ["project_path"],
67
+ },
68
+ },
69
+ ],
70
+ }));
71
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
72
+ const { name, arguments: args } = request.params;
73
+ try {
74
+ let result;
75
+ if (name === "read_module") {
76
+ result = await (0, read_module_1.readModule)(args);
77
+ }
78
+ else if (name === "list_modules") {
79
+ result = await (0, list_modules_1.listModules)(args);
80
+ }
81
+ else if (name === "write_spec") {
82
+ result = await (0, write_spec_1.writeSpec)(args);
83
+ }
84
+ else if (name === "read_specs") {
85
+ result = await (0, read_specs_1.readSpecs)(args);
86
+ }
87
+ else {
88
+ result = { error: `Unknown tool: ${name}` };
89
+ }
90
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
91
+ }
92
+ catch (e) {
93
+ return { content: [{ type: "text", text: JSON.stringify({ error: e?.message ?? String(e) }) }] };
94
+ }
95
+ });
96
+ async function main() {
97
+ const transport = new stdio_js_1.StdioServerTransport();
98
+ await server.connect(transport);
99
+ }
100
+ main().catch((e) => {
101
+ process.stderr.write(`DocuFlow MCP fatal error: ${e?.message ?? e}\n`);
102
+ process.exit(1);
103
+ });
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.extensionToLanguage = extensionToLanguage;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const EXT_MAP = {
9
+ ".cs": "csharp",
10
+ ".vb": "vbnet",
11
+ ".fs": "fsharp",
12
+ ".ts": "typescript",
13
+ ".tsx": "typescript",
14
+ ".js": "javascript",
15
+ ".jsx": "javascript",
16
+ ".mjs": "javascript",
17
+ ".cjs": "javascript",
18
+ ".php": "php",
19
+ ".java": "java",
20
+ ".py": "python",
21
+ ".rb": "ruby",
22
+ ".go": "go",
23
+ ".rs": "rust",
24
+ ".kt": "kotlin",
25
+ ".swift": "swift",
26
+ ".html": "html",
27
+ ".htm": "html",
28
+ ".cshtml": "razor",
29
+ ".razor": "razor",
30
+ ".vue": "vue",
31
+ ".css": "css",
32
+ ".scss": "scss",
33
+ ".sass": "sass",
34
+ ".less": "less",
35
+ ".json": "json",
36
+ ".xml": "xml",
37
+ ".yml": "yaml",
38
+ ".yaml": "yaml",
39
+ ".sql": "sql",
40
+ ".sh": "shell",
41
+ ".bash": "shell",
42
+ ".ps1": "powershell",
43
+ ".md": "markdown",
44
+ ".c": "c",
45
+ ".h": "c",
46
+ ".cpp": "cpp",
47
+ ".hpp": "cpp",
48
+ ".cc": "cpp",
49
+ };
50
+ function extensionToLanguage(filePath) {
51
+ const base = node_path_1.default.basename(filePath).toLowerCase();
52
+ // Angular pattern detection — checked first, before generic .ts.
53
+ if (base.endsWith(".component.ts") || base.endsWith(".service.ts") || base.endsWith(".module.ts") || base.endsWith(".directive.ts") || base.endsWith(".pipe.ts") || base.endsWith(".guard.ts")) {
54
+ return "angular";
55
+ }
56
+ const ext = node_path_1.default.extname(base);
57
+ return EXT_MAP[ext] ?? "unknown";
58
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.listModules = listModules;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const filesystem_1 = require("../filesystem");
9
+ const language_map_1 = require("../language-map");
10
+ const extractor_1 = require("../extractor");
11
+ async function listModules(input) {
12
+ const projectPath = node_path_1.default.resolve(input.path);
13
+ const exts = input.extensions?.map((e) => (e.startsWith(".") ? e.toLowerCase() : "." + e.toLowerCase()));
14
+ const { files, skipped } = await (0, filesystem_1.walk)(projectPath);
15
+ const allSkipped = [...skipped];
16
+ const modules = [];
17
+ const langs = new Set();
18
+ for (const f of files) {
19
+ if (exts && !exts.includes(node_path_1.default.extname(f.path).toLowerCase())) {
20
+ allSkipped.push({ path: f.path, reason: "extension filter" });
21
+ continue;
22
+ }
23
+ const read = await (0, filesystem_1.safeReadFile)(f.path);
24
+ if (read.error) {
25
+ allSkipped.push({ path: f.path, reason: `read failed: ${read.error}` });
26
+ continue;
27
+ }
28
+ if (read.binary) {
29
+ allSkipped.push({ path: f.path, reason: "binary" });
30
+ continue;
31
+ }
32
+ const language = (0, language_map_1.extensionToLanguage)(f.path);
33
+ langs.add(language);
34
+ const facts = (0, extractor_1.extract)(read.content ?? "");
35
+ modules.push({
36
+ path: node_path_1.default.relative(projectPath, f.path) || f.path,
37
+ language,
38
+ size_bytes: read.size,
39
+ ...facts,
40
+ });
41
+ }
42
+ return {
43
+ scanned_at: new Date().toISOString(),
44
+ project_path: projectPath,
45
+ total_files: modules.length,
46
+ skipped_files: allSkipped,
47
+ languages_found: Array.from(langs).sort(),
48
+ modules,
49
+ };
50
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readModule = readModule;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const filesystem_1 = require("../filesystem");
9
+ const language_map_1 = require("../language-map");
10
+ const extractor_1 = require("../extractor");
11
+ const MAX_RAW = 8000;
12
+ async function readModule(input) {
13
+ const filePath = node_path_1.default.resolve(input.path);
14
+ const language = (0, language_map_1.extensionToLanguage)(filePath);
15
+ const read = await (0, filesystem_1.safeReadFile)(filePath);
16
+ if (read.error) {
17
+ return {
18
+ path: filePath,
19
+ language,
20
+ size_bytes: 0,
21
+ classes: [],
22
+ functions: [],
23
+ dependencies: [],
24
+ db_tables: [],
25
+ endpoints: [],
26
+ config_refs: [],
27
+ error: read.error,
28
+ };
29
+ }
30
+ if (read.binary) {
31
+ return {
32
+ path: filePath,
33
+ language,
34
+ size_bytes: read.size,
35
+ classes: [],
36
+ functions: [],
37
+ dependencies: [],
38
+ db_tables: [],
39
+ endpoints: [],
40
+ config_refs: [],
41
+ error: "binary file skipped",
42
+ };
43
+ }
44
+ const content = read.content ?? "";
45
+ const facts = (0, extractor_1.extract)(content);
46
+ return {
47
+ path: filePath,
48
+ language,
49
+ size_bytes: read.size,
50
+ ...facts,
51
+ raw_content: content.length > MAX_RAW ? content.slice(0, MAX_RAW) : content,
52
+ };
53
+ }
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readSpecs = readSpecs;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ const filesystem_1 = require("../filesystem");
10
+ async function readSpecs(input) {
11
+ const projectPath = node_path_1.default.resolve(input.project_path);
12
+ const docuDir = node_path_1.default.join(projectPath, ".docuflow");
13
+ const indexPath = node_path_1.default.join(docuDir, "index.json");
14
+ const index = await (0, filesystem_1.readJsonIfExists)(indexPath);
15
+ if (!index || index.specs.length === 0) {
16
+ return { specs_found: 0, specs: [] };
17
+ }
18
+ let entries = index.specs;
19
+ if (input.module_name) {
20
+ const needle = input.module_name.replace(/\.md$/i, "").toLowerCase();
21
+ entries = entries.filter((s) => s.filename.replace(/\.md$/i, "").toLowerCase() === needle);
22
+ }
23
+ const specs = [];
24
+ for (const entry of entries) {
25
+ const filePath = node_path_1.default.join(docuDir, "specs", entry.filename);
26
+ try {
27
+ const content = await promises_1.default.readFile(filePath, "utf8");
28
+ specs.push({ filename: entry.filename, written_at: entry.written_at, content });
29
+ }
30
+ catch {
31
+ // spec entry exists in index but file missing — skip silently
32
+ }
33
+ }
34
+ return { specs_found: specs.length, specs };
35
+ }
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.writeSpec = writeSpec;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const filesystem_1 = require("../filesystem");
9
+ const promises_1 = __importDefault(require("node:fs/promises"));
10
+ // Per-project write lock: maps projectPath → promise chain so that concurrent
11
+ // calls to writeSpec on the same project are always serialised. This prevents
12
+ // the read-modify-write on index.json from racing when the agent issues several
13
+ // write_spec calls in parallel (e.g. writing one spec per module at once).
14
+ const indexLocks = new Map();
15
+ function withLock(key, fn) {
16
+ const prev = indexLocks.get(key) ?? Promise.resolve();
17
+ const next = prev.then(fn).catch(() => { });
18
+ indexLocks.set(key, next);
19
+ return next;
20
+ }
21
+ async function writeSpec(input) {
22
+ const projectPath = node_path_1.default.resolve(input.project_path);
23
+ const docuDir = node_path_1.default.join(projectPath, ".docuflow");
24
+ const specsDir = node_path_1.default.join(docuDir, "specs");
25
+ await (0, filesystem_1.ensureDir)(specsDir);
26
+ const cleanName = input.filename.replace(/\.md$/i, "");
27
+ const targetFile = node_path_1.default.join(specsDir, `${cleanName}.md`);
28
+ // Write the markdown file immediately — each spec file is independent so
29
+ // parallel writes to different filenames are safe without locking.
30
+ const bytes = await (0, filesystem_1.writeFileAtomic)(targetFile, input.content);
31
+ // Serialise index updates per project to avoid read-modify-write races.
32
+ const indexPath = node_path_1.default.join(docuDir, "index.json");
33
+ let indexUpdated = false;
34
+ let writeError;
35
+ await withLock(projectPath, async () => {
36
+ try {
37
+ const existing = (await (0, filesystem_1.readJsonIfExists)(indexPath)) ?? { specs: [] };
38
+ const now = new Date().toISOString();
39
+ const idx = existing.specs.findIndex((s) => s.filename === `${cleanName}.md`);
40
+ if (idx >= 0)
41
+ existing.specs[idx].written_at = now;
42
+ else
43
+ existing.specs.push({ filename: `${cleanName}.md`, written_at: now });
44
+ await promises_1.default.writeFile(indexPath, JSON.stringify(existing, null, 2), "utf8");
45
+ indexUpdated = true;
46
+ }
47
+ catch (e) {
48
+ writeError = e?.message ?? String(e);
49
+ }
50
+ });
51
+ if (writeError) {
52
+ return { written_to: targetFile, bytes_written: bytes, index_updated: false, error: writeError };
53
+ }
54
+ return { written_to: targetFile, bytes_written: bytes, index_updated: indexUpdated };
55
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@doquflow/server",
3
+ "version": "0.1.0",
4
+ "description": "Docuflow MCP server — lets AI agents read codebases and persist living specs",
5
+ "author": "Docuflow <hello@doquflows.dev>",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/doquflows/docuflow",
8
+ "repository": { "type": "git", "url": "https://github.com/doquflows/docuflow.git" },
9
+ "bugs": { "url": "https://github.com/doquflows/docuflow/issues" },
10
+ "keywords": ["mcp", "ai-agents", "claude-code", "documentation", "codebase", "developer-tools"],
11
+ "bin": { "docuflow-server": "./dist/index.js" },
12
+ "files": ["dist/**"],
13
+ "scripts": { "build": "tsc" },
14
+ "dependencies": { "@modelcontextprotocol/sdk": "^1.0.4" },
15
+ "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.6.0" }
16
+ }