Initial commit
This commit is contained in:
46
mcp-server/tools/files/delete.js
Normal file
46
mcp-server/tools/files/delete.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { z } from "zod";
|
||||
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
|
||||
import { getCurrentProjectInfo, callLocalFileEndpoint, buildLocalFileErrorResponse } from "./helpers.js";
|
||||
|
||||
export function registerAcaiDeleteTool(server) {
|
||||
server.tool(
|
||||
"acai-delete",
|
||||
"Delete a file inside the project. Destructive operation.",
|
||||
{
|
||||
file_path: z.string().describe("Path relative to the project root"),
|
||||
expected_sha256: z.string().optional().describe("Optional safety check before deletion"),
|
||||
},
|
||||
{ readOnlyHint: false, destructiveHint: true },
|
||||
async ({ file_path, expected_sha256 }) => {
|
||||
try {
|
||||
const validationError = validateRequired({ file_path }, ["file_path"], "acai-delete");
|
||||
if (validationError) return validationError;
|
||||
|
||||
const { projectSlug, projectDir } = getCurrentProjectInfo();
|
||||
const result = await callLocalFileEndpoint("POST", "/api/files/delete", {
|
||||
project: projectSlug,
|
||||
projectDir: projectDir,
|
||||
relativePath: file_path,
|
||||
expectedSha256: expected_sha256 || "",
|
||||
});
|
||||
if (!result.data?.success) {
|
||||
return buildLocalFileErrorResponse("acai-delete", result, { file_path });
|
||||
}
|
||||
const data = result.data;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
file_path: data.filePath,
|
||||
deleted: data.deleted,
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
return handleToolError(error, "acai-delete", { file_path });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
51
mcp-server/tools/files/glob.js
Normal file
51
mcp-server/tools/files/glob.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { z } from "zod";
|
||||
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
|
||||
import { getCurrentProjectInfo, callLocalFileEndpoint, buildLocalFileErrorResponse } from "./helpers.js";
|
||||
|
||||
export function registerAcaiGlobTool(server) {
|
||||
server.tool(
|
||||
"acai-glob",
|
||||
"Find project files by path pattern. Returns compact relative paths only.",
|
||||
{
|
||||
pattern: z.string().describe("Glob-style pattern relative to the project root, e.g. 'template/estandar/modulos/**/index-base.tpl'"),
|
||||
base_path: z.string().optional().describe("Optional base directory relative to the project root"),
|
||||
limit: z.number().int().positive().max(200).optional().describe("Maximum number of paths to return"),
|
||||
},
|
||||
{ readOnlyHint: true, destructiveHint: false },
|
||||
async ({ pattern, base_path, limit }) => {
|
||||
try {
|
||||
const validationError = validateRequired({ pattern }, ["pattern"], "acai-glob");
|
||||
if (validationError) return validationError;
|
||||
|
||||
const { projectSlug, projectDir } = getCurrentProjectInfo();
|
||||
const result = await callLocalFileEndpoint("GET", "/api/files/glob", null, {
|
||||
project: projectSlug,
|
||||
projectDir,
|
||||
pattern,
|
||||
basePath: base_path,
|
||||
limit,
|
||||
});
|
||||
if (!result.data?.success) {
|
||||
return buildLocalFileErrorResponse("acai-glob", result, { pattern, base_path });
|
||||
}
|
||||
const data = result.data;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
pattern: data.pattern,
|
||||
base_path: data.basePath,
|
||||
matches: data.matches,
|
||||
total_matches: data.totalMatches,
|
||||
truncated: data.truncated,
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
return handleToolError(error, "acai-glob", { pattern, base_path });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
65
mcp-server/tools/files/grep.js
Normal file
65
mcp-server/tools/files/grep.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import { z } from "zod";
|
||||
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
|
||||
import { getCurrentProjectInfo, callLocalFileEndpoint, buildLocalFileErrorResponse } from "./helpers.js";
|
||||
|
||||
export function registerAcaiGrepTool(server) {
|
||||
server.tool(
|
||||
"acai-grep",
|
||||
"Search text inside project files with compact line-level results. Supports optional glob filtering.",
|
||||
{
|
||||
pattern: z.string().describe("Text or regex pattern to search for"),
|
||||
base_path: z.string().optional().describe("Optional base directory relative to the project root"),
|
||||
glob: z.string().optional().describe("Optional file path glob filter, e.g. '**/index-base.tpl'"),
|
||||
limit: z.number().int().positive().max(100).optional().describe("Maximum number of matches to return"),
|
||||
case_sensitive: z.boolean().optional().describe("Whether matching should be case-sensitive"),
|
||||
regex: z.boolean().optional().describe("Treat pattern as a regular expression"),
|
||||
},
|
||||
{ readOnlyHint: true, destructiveHint: false },
|
||||
async ({ pattern, base_path, glob, limit, case_sensitive, regex }) => {
|
||||
try {
|
||||
const validationError = validateRequired({ pattern }, ["pattern"], "acai-grep");
|
||||
if (validationError) return validationError;
|
||||
|
||||
const { projectSlug, projectDir } = getCurrentProjectInfo();
|
||||
const result = await callLocalFileEndpoint("POST", "/api/files/grep", {
|
||||
project: projectSlug,
|
||||
projectDir,
|
||||
pattern,
|
||||
basePath: base_path,
|
||||
glob,
|
||||
limit,
|
||||
caseSensitive: case_sensitive,
|
||||
regex,
|
||||
});
|
||||
if (!result.data?.success) {
|
||||
return buildLocalFileErrorResponse("acai-grep", result, { pattern, base_path, glob });
|
||||
}
|
||||
const data = result.data;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
pattern: data.pattern,
|
||||
base_path: data.basePath,
|
||||
glob: data.glob,
|
||||
regex: data.regex,
|
||||
case_sensitive: data.caseSensitive,
|
||||
files_scanned: data.filesScanned,
|
||||
matches: data.matches.map((match) => ({
|
||||
file_path: match.filePath,
|
||||
line: match.line,
|
||||
match_preview: match.matchPreview,
|
||||
})),
|
||||
total_matches: data.totalMatches,
|
||||
truncated: data.truncated,
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
return handleToolError(error, "acai-grep", { pattern, base_path, glob });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
60
mcp-server/tools/files/helpers.js
Normal file
60
mcp-server/tools/files/helpers.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import axios from "axios";
|
||||
import path from "path";
|
||||
import { LOCAL_SERVER_URL, getLocalServerHeaders } from "../../config/index.js";
|
||||
|
||||
export function getCurrentProjectInfo() {
|
||||
const projectDir = process.env.ACAI_PROJECT_DIR || "";
|
||||
if (!projectDir) {
|
||||
throw new Error("ACAI_PROJECT_DIR not set");
|
||||
}
|
||||
return {
|
||||
projectDir,
|
||||
projectSlug: path.basename(path.resolve(projectDir)),
|
||||
};
|
||||
}
|
||||
|
||||
export async function callLocalFileEndpoint(method, endpoint, payload = null, query = null) {
|
||||
const headers = getLocalServerHeaders();
|
||||
if (method === "GET") {
|
||||
const response = await axios.get(`${LOCAL_SERVER_URL}${endpoint}`, {
|
||||
params: query || undefined,
|
||||
headers,
|
||||
timeout: 30000,
|
||||
validateStatus: (status) => status < 600,
|
||||
});
|
||||
return { status: response.status, data: response.data };
|
||||
}
|
||||
|
||||
const response = await axios.post(`${LOCAL_SERVER_URL}${endpoint}`, payload || {}, {
|
||||
headers,
|
||||
timeout: 30000,
|
||||
validateStatus: (status) => status < 600,
|
||||
});
|
||||
return { status: response.status, data: response.data };
|
||||
}
|
||||
|
||||
export function buildLocalFileErrorResponse(toolName, result, extra = {}) {
|
||||
const payload = result?.data || {};
|
||||
const message =
|
||||
payload.message ||
|
||||
payload.error ||
|
||||
payload.compileError ||
|
||||
`HTTP ${result?.status || 500}`;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: false,
|
||||
error: {
|
||||
code: `HTTP_${result.status}`,
|
||||
message,
|
||||
context: toolName,
|
||||
...extra,
|
||||
...payload,
|
||||
},
|
||||
}, null, 2),
|
||||
}],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
15
mcp-server/tools/files/index.js
Normal file
15
mcp-server/tools/files/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { registerAcaiViewTool } from "./view.js";
|
||||
import { registerAcaiWriteTool } from "./write.js";
|
||||
import { registerAcaiLineReplaceTool } from "./lineReplace.js";
|
||||
import { registerAcaiDeleteTool } from "./delete.js";
|
||||
import { registerAcaiGlobTool } from "./glob.js";
|
||||
import { registerAcaiGrepTool } from "./grep.js";
|
||||
|
||||
export function registerFileTools(server) {
|
||||
registerAcaiViewTool(server);
|
||||
registerAcaiGlobTool(server);
|
||||
registerAcaiGrepTool(server);
|
||||
registerAcaiWriteTool(server);
|
||||
registerAcaiLineReplaceTool(server);
|
||||
registerAcaiDeleteTool(server);
|
||||
}
|
||||
67
mcp-server/tools/files/lineReplace.js
Normal file
67
mcp-server/tools/files/lineReplace.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { z } from "zod";
|
||||
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
|
||||
import { getCurrentProjectInfo, callLocalFileEndpoint, buildLocalFileErrorResponse } from "./helpers.js";
|
||||
|
||||
export function registerAcaiLineReplaceTool(server) {
|
||||
server.tool(
|
||||
"acai-line-replace",
|
||||
"Replace a validated line block in an existing file. Preferred for editing existing files while minimizing token usage.",
|
||||
{
|
||||
file_path: z.string().describe("Path relative to the project root"),
|
||||
first_replaced_line: z.number().int().positive().describe("1-indexed first line of the target block"),
|
||||
last_replaced_line: z.number().int().positive().describe("1-indexed last line of the target block"),
|
||||
search: z.string().describe("Expected current content for validation. Must match the selected block exactly."),
|
||||
replace: z.string().describe("Replacement content for the selected block"),
|
||||
expected_sha256: z.string().optional().describe("Optional full-file sha check from a prior acai-view"),
|
||||
},
|
||||
{ readOnlyHint: false, destructiveHint: false },
|
||||
async ({ file_path, first_replaced_line, last_replaced_line, search, replace, expected_sha256 }) => {
|
||||
try {
|
||||
const validationError = validateRequired(
|
||||
{ file_path, first_replaced_line, last_replaced_line, search },
|
||||
["file_path", "first_replaced_line", "last_replaced_line", "search"],
|
||||
"acai-line-replace"
|
||||
);
|
||||
if (validationError) return validationError;
|
||||
|
||||
const { projectSlug, projectDir } = getCurrentProjectInfo();
|
||||
const result = await callLocalFileEndpoint("POST", "/api/files/line-replace", {
|
||||
project: projectSlug,
|
||||
projectDir: projectDir,
|
||||
relativePath: file_path,
|
||||
firstLine: first_replaced_line,
|
||||
lastLine: last_replaced_line,
|
||||
search,
|
||||
replace,
|
||||
expectedSha256: expected_sha256 || "",
|
||||
});
|
||||
if (!result.data?.success) {
|
||||
return buildLocalFileErrorResponse("acai-line-replace", result, {
|
||||
file_path,
|
||||
first_replaced_line,
|
||||
last_replaced_line,
|
||||
});
|
||||
}
|
||||
const data = result.data;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
file_path: data.filePath,
|
||||
first_replaced_line: data.firstLine,
|
||||
last_replaced_line: data.lastLine,
|
||||
new_sha256: data.newSha256,
|
||||
changed: data.changed,
|
||||
compiled: data.compiled || false,
|
||||
compile_result: data.compileResult || null,
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
return handleToolError(error, "acai-line-replace", { file_path, first_replaced_line, last_replaced_line });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
60
mcp-server/tools/files/view.js
Normal file
60
mcp-server/tools/files/view.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { z } from "zod";
|
||||
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
|
||||
import { getCurrentProjectInfo, callLocalFileEndpoint, buildLocalFileErrorResponse } from "./helpers.js";
|
||||
|
||||
export function registerAcaiViewTool(server) {
|
||||
server.tool(
|
||||
"acai-view",
|
||||
"Read a project file with optional line ranges. Returns only the requested slice plus compact metadata for safe incremental edits.",
|
||||
{
|
||||
file_path: z.string().describe("Path relative to the project root"),
|
||||
start_line: z.number().int().positive().optional().describe("1-indexed start line. Defaults to 1."),
|
||||
end_line: z.number().int().positive().optional().describe("1-indexed end line. If omitted, the server returns a bounded chunk."),
|
||||
},
|
||||
{ readOnlyHint: true, destructiveHint: false },
|
||||
async ({ file_path, start_line, end_line }) => {
|
||||
try {
|
||||
const validationError = validateRequired({ file_path }, ["file_path"], "acai-view");
|
||||
if (validationError) return validationError;
|
||||
|
||||
if (end_line !== undefined && start_line !== undefined && end_line < start_line) {
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify({ success: false, error: "end_line must be greater than or equal to start_line" }, null, 2) }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const { projectSlug } = getCurrentProjectInfo();
|
||||
const result = await callLocalFileEndpoint("GET", "/api/files/read", null, {
|
||||
project: projectSlug,
|
||||
projectDir: getCurrentProjectInfo().projectDir,
|
||||
relativePath: file_path,
|
||||
startLine: start_line,
|
||||
endLine: end_line,
|
||||
});
|
||||
if (!result.data?.success) {
|
||||
return buildLocalFileErrorResponse("acai-view", result, { file_path });
|
||||
}
|
||||
const data = result.data;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
file_path: data.filePath,
|
||||
start_line: data.startLine,
|
||||
end_line: data.endLine,
|
||||
total_lines: data.totalLines,
|
||||
sha256: data.sha256,
|
||||
content: data.content,
|
||||
truncated: data.truncated,
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
return handleToolError(error, "acai-view", { file_path });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
59
mcp-server/tools/files/write.js
Normal file
59
mcp-server/tools/files/write.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { z } from "zod";
|
||||
import { handleToolError, validateRequired } from "../helpers/errorHandler.js";
|
||||
import { getCurrentProjectInfo, callLocalFileEndpoint, buildLocalFileErrorResponse } from "./helpers.js";
|
||||
|
||||
export function registerAcaiWriteTool(server) {
|
||||
server.tool(
|
||||
"acai-write",
|
||||
`Write a full file inside the project. Use for new files or full rewrites. Prefer acai-line-replace for targeted edits.
|
||||
|
||||
Before writing, check the matching documentation for the file type:
|
||||
- If the file is an index-base template (\`index-base.tpl\` or \`index-base.html\`), make sure you have read \`docs/module-creation-guide.md\` and \`docs/builder-fields.md\`
|
||||
- If the file is a \`.js\` or \`.css\`, make sure you have read \`docs/css-js-conventions.md\`
|
||||
- If the file is a module or global hook PHP file, make sure you have read \`docs/hooks-and-api.md\``,
|
||||
{
|
||||
file_path: z.string().describe("Path relative to the project root"),
|
||||
content: z.string().describe("Full file content to write"),
|
||||
mode: z.enum(["create_or_overwrite", "create_only"]).optional().default("create_or_overwrite").describe("Write mode"),
|
||||
expected_sha256: z.string().optional().describe("Optional optimistic concurrency check from a prior acai-view"),
|
||||
},
|
||||
{ readOnlyHint: false, destructiveHint: false },
|
||||
async ({ file_path, content, mode = "create_or_overwrite", expected_sha256 }) => {
|
||||
try {
|
||||
const validationError = validateRequired({ file_path }, ["file_path"], "acai-write");
|
||||
if (validationError) return validationError;
|
||||
|
||||
const { projectSlug, projectDir } = getCurrentProjectInfo();
|
||||
const result = await callLocalFileEndpoint("POST", "/api/files/write", {
|
||||
project: projectSlug,
|
||||
projectDir: projectDir,
|
||||
relativePath: file_path,
|
||||
content,
|
||||
mode,
|
||||
expectedSha256: expected_sha256 || "",
|
||||
});
|
||||
if (!result.data?.success) {
|
||||
return buildLocalFileErrorResponse("acai-write", result, { file_path });
|
||||
}
|
||||
const data = result.data;
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({
|
||||
success: true,
|
||||
file_path: data.filePath,
|
||||
created: data.created,
|
||||
overwritten: data.overwritten,
|
||||
sha256: data.sha256,
|
||||
compiled: data.compiled || false,
|
||||
compile_result: data.compileResult || null,
|
||||
}, null, 2),
|
||||
}],
|
||||
};
|
||||
} catch (error) {
|
||||
return handleToolError(error, "acai-write", { file_path });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user