atabey-mcp 0.0.4
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/dist/constants.js +64 -0
- package/dist/index.js +119 -0
- package/dist/tools/control_plane/locking.js +82 -0
- package/dist/tools/control_plane/registry.js +34 -0
- package/dist/tools/definitions.js +290 -0
- package/dist/tools/file_system/batch_surgical_edit.js +59 -0
- package/dist/tools/file_system/patch_file.js +29 -0
- package/dist/tools/file_system/read_file.js +51 -0
- package/dist/tools/file_system/replace_text.js +45 -0
- package/dist/tools/file_system/write_file.js +38 -0
- package/dist/tools/framework/audit_deps.js +41 -0
- package/dist/tools/framework/get_status.js +5 -0
- package/dist/tools/framework/orchestrate.js +5 -0
- package/dist/tools/framework/run_tests.js +27 -0
- package/dist/tools/framework/update_contract_hash.js +5 -0
- package/dist/tools/framework/update_memory.js +8 -0
- package/dist/tools/index.js +60 -0
- package/dist/tools/memory/get_insights.js +34 -0
- package/dist/tools/memory/read_memory.js +28 -0
- package/dist/tools/messaging/log_action.js +22 -0
- package/dist/tools/messaging/send_message.js +94 -0
- package/dist/tools/observability/check_ports.js +26 -0
- package/dist/tools/observability/get_health.js +19 -0
- package/dist/tools/quality/check_lint.js +30 -0
- package/dist/tools/search/get_gaps.js +48 -0
- package/dist/tools/search/get_map.js +43 -0
- package/dist/tools/search/grep_search.js +75 -0
- package/dist/tools/search/list_dir.js +28 -0
- package/dist/tools/shell/run_command.js +56 -0
- package/dist/tools/types.js +1 -0
- package/dist/utils/cli.js +59 -0
- package/dist/utils/compliance.js +78 -0
- package/dist/utils/fs.js +44 -0
- package/dist/utils/metrics.js +56 -0
- package/dist/utils/security.js +60 -0
- package/package.json +26 -0
- package/src/constants.ts +78 -0
- package/src/declarations.d.ts +17 -0
- package/src/index.ts +144 -0
- package/src/tools/control_plane/locking.ts +89 -0
- package/src/tools/control_plane/registry.ts +38 -0
- package/src/tools/definitions.ts +292 -0
- package/src/tools/file_system/batch_surgical_edit.ts +79 -0
- package/src/tools/file_system/patch_file.ts +39 -0
- package/src/tools/file_system/read_file.ts +58 -0
- package/src/tools/file_system/replace_text.ts +54 -0
- package/src/tools/file_system/write_file.ts +45 -0
- package/src/tools/framework/audit_deps.ts +49 -0
- package/src/tools/framework/get_status.ts +7 -0
- package/src/tools/framework/orchestrate.ts +7 -0
- package/src/tools/framework/run_tests.ts +30 -0
- package/src/tools/framework/update_contract_hash.ts +7 -0
- package/src/tools/framework/update_memory.ts +10 -0
- package/src/tools/index.ts +64 -0
- package/src/tools/memory/get_insights.ts +41 -0
- package/src/tools/memory/read_memory.ts +31 -0
- package/src/tools/messaging/log_action.ts +28 -0
- package/src/tools/messaging/send_message.ts +97 -0
- package/src/tools/observability/check_ports.ts +30 -0
- package/src/tools/observability/get_health.ts +24 -0
- package/src/tools/quality/check_lint.ts +36 -0
- package/src/tools/search/get_gaps.ts +54 -0
- package/src/tools/search/get_map.ts +48 -0
- package/src/tools/search/grep_search.ts +75 -0
- package/src/tools/search/list_dir.ts +34 -0
- package/src/tools/shell/run_command.ts +66 -0
- package/src/tools/types.ts +89 -0
- package/src/utils/cli.ts +53 -0
- package/src/utils/compliance.ts +95 -0
- package/src/utils/fs.ts +45 -0
- package/src/utils/metrics.ts +73 -0
- package/src/utils/security.ts +66 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Generates a tree-view map of the project structure.
|
|
5
|
+
* Helps agents visualize the entire project layout quickly.
|
|
6
|
+
*/
|
|
7
|
+
export function handleGetProjectMap(projectRoot, args) {
|
|
8
|
+
const maxDepth = args.maxDepth || 3;
|
|
9
|
+
const includeFiles = args.includeFiles !== false;
|
|
10
|
+
const buildTree = (dir, depth) => {
|
|
11
|
+
if (depth > maxDepth)
|
|
12
|
+
return [];
|
|
13
|
+
const results = [];
|
|
14
|
+
const files = fs.readdirSync(dir);
|
|
15
|
+
files.forEach(file => {
|
|
16
|
+
if (file === "node_modules" || file === ".git" || file === "dist" || file.startsWith("."))
|
|
17
|
+
return;
|
|
18
|
+
const fullPath = path.join(dir, file);
|
|
19
|
+
const stat = fs.statSync(fullPath);
|
|
20
|
+
const indent = " ".repeat(depth);
|
|
21
|
+
if (stat.isDirectory()) {
|
|
22
|
+
results.push(`${indent}📁 ${file}/`);
|
|
23
|
+
results.push(...buildTree(fullPath, depth + 1));
|
|
24
|
+
}
|
|
25
|
+
else if (includeFiles) {
|
|
26
|
+
results.push(`${indent}📄 ${file}`);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return results;
|
|
30
|
+
};
|
|
31
|
+
try {
|
|
32
|
+
const tree = buildTree(projectRoot, 0);
|
|
33
|
+
return {
|
|
34
|
+
content: [{
|
|
35
|
+
type: "text",
|
|
36
|
+
text: `🗺️ **Project Map (Depth: ${maxDepth})**\n\n${tree.join("\n")}`
|
|
37
|
+
}]
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
return { isError: true, content: [{ type: "text", text: `Failed to map project: ${String(e)}` }] };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { Metrics } from "../../utils/metrics.js";
|
|
4
|
+
/**
|
|
5
|
+
* Searches for a regex pattern within files in the project.
|
|
6
|
+
*/
|
|
7
|
+
export function handleGrepSearch(projectRoot, args) {
|
|
8
|
+
const pattern = args.pattern;
|
|
9
|
+
const includePattern = args.includePattern || ""; // e.g., ".ts"
|
|
10
|
+
const excludePattern = args.excludePattern || "node_modules";
|
|
11
|
+
if (!pattern) {
|
|
12
|
+
const err = "Search pattern is required.";
|
|
13
|
+
Metrics.logError(projectRoot, "@mcp", "grep_search", err);
|
|
14
|
+
return { isError: true, content: [{ type: "text", text: `❌ ${err}` }] };
|
|
15
|
+
}
|
|
16
|
+
const results = [];
|
|
17
|
+
try {
|
|
18
|
+
new RegExp(pattern);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
const err = `Invalid regex pattern: ${String(e)}`;
|
|
22
|
+
Metrics.logError(projectRoot, "@mcp", "grep_search", err);
|
|
23
|
+
return { isError: true, content: [{ type: "text", text: `❌ ${err}` }] };
|
|
24
|
+
}
|
|
25
|
+
const walk = (dir) => {
|
|
26
|
+
if (results.length > 100)
|
|
27
|
+
return;
|
|
28
|
+
try {
|
|
29
|
+
const files = fs.readdirSync(dir);
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
if (results.length > 100)
|
|
32
|
+
return;
|
|
33
|
+
const filePath = path.join(dir, file);
|
|
34
|
+
if (excludePattern && filePath.includes(excludePattern)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const stat = fs.statSync(filePath);
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
walk(filePath);
|
|
40
|
+
}
|
|
41
|
+
else if (stat.isFile()) {
|
|
42
|
+
if (includePattern && !filePath.endsWith(includePattern)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
46
|
+
// Create a new regex object for each line to avoid state issues with /g
|
|
47
|
+
if (new RegExp(pattern).test(content)) {
|
|
48
|
+
if (results.length < 100) {
|
|
49
|
+
results.push(filePath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Ignore directories that cannot be read
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
walk(projectRoot);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
const err = `Search failed: ${String(e)}`;
|
|
64
|
+
Metrics.logError(projectRoot, "@mcp", "grep_search", err);
|
|
65
|
+
return { isError: true, content: [{ type: "text", text: `❌ ${err}` }] };
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
content: [{
|
|
69
|
+
type: "text",
|
|
70
|
+
text: results.length > 0
|
|
71
|
+
? `Found ${results.length} matches:\n\n${results.join("\n")}`
|
|
72
|
+
: "No matches found."
|
|
73
|
+
}]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { safePath } from "../../utils/security.js";
|
|
4
|
+
/**
|
|
5
|
+
* Lists the contents of a directory.
|
|
6
|
+
*/
|
|
7
|
+
export function handleListDir(projectRoot, args) {
|
|
8
|
+
const dirPath = safePath(projectRoot, args.path || ".");
|
|
9
|
+
if (!fs.existsSync(dirPath)) {
|
|
10
|
+
throw new Error(`Directory not found: ${args.path}`);
|
|
11
|
+
}
|
|
12
|
+
const stats = fs.statSync(dirPath);
|
|
13
|
+
if (!stats.isDirectory()) {
|
|
14
|
+
throw new Error(`Path is not a directory: ${args.path}`);
|
|
15
|
+
}
|
|
16
|
+
const files = fs.readdirSync(dirPath);
|
|
17
|
+
const results = files.map(file => {
|
|
18
|
+
const fullPath = path.join(dirPath, file);
|
|
19
|
+
const isDir = fs.statSync(fullPath).isDirectory();
|
|
20
|
+
return `${isDir ? "[DIR] " : " "}${file}`;
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: "text",
|
|
25
|
+
text: `Directory listing for ${args.path || "."}:\n\n${results.join("\n")}`
|
|
26
|
+
}]
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { Metrics } from "../../utils/metrics.js";
|
|
3
|
+
const COMMAND_ALLOW_LIST = [
|
|
4
|
+
"npm test",
|
|
5
|
+
"npm run lint",
|
|
6
|
+
"npm run build",
|
|
7
|
+
"git status",
|
|
8
|
+
"git diff",
|
|
9
|
+
"npx vitest run",
|
|
10
|
+
"go test",
|
|
11
|
+
"go fmt",
|
|
12
|
+
"go build",
|
|
13
|
+
"pytest",
|
|
14
|
+
"ruff check",
|
|
15
|
+
"dotnet test",
|
|
16
|
+
"dotnet format",
|
|
17
|
+
"dotnet build",
|
|
18
|
+
"./gradlew",
|
|
19
|
+
"mvn",
|
|
20
|
+
];
|
|
21
|
+
const TIMEOUT = 30000; // 30 seconds
|
|
22
|
+
export function handleRunCommand(projectRoot, args) {
|
|
23
|
+
const command = args.command;
|
|
24
|
+
const isAllowed = COMMAND_ALLOW_LIST.some(allowedCmd => command.startsWith(allowedCmd));
|
|
25
|
+
if (!isAllowed) {
|
|
26
|
+
const errorMsg = `Command not allowed: "${command}". Only commands starting with the following are allowed: ${COMMAND_ALLOW_LIST.join(", ")}`;
|
|
27
|
+
Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command} (denied)`, errorMsg);
|
|
28
|
+
return Promise.resolve({
|
|
29
|
+
content: [{ type: "text", text: `ERROR: ${errorMsg}` }],
|
|
30
|
+
isError: true,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
exec(command, { cwd: projectRoot, timeout: TIMEOUT }, (error, stdout, stderr) => {
|
|
35
|
+
const output = stdout + stderr;
|
|
36
|
+
const tokens = Metrics.estimateTokens(output);
|
|
37
|
+
Metrics.logUsage(projectRoot, "@mcp", `run_shell_command: ${command}`, tokens);
|
|
38
|
+
if (error) {
|
|
39
|
+
const errorMsg = `Command failed with exit code ${error.code}: ${error.message}.`;
|
|
40
|
+
Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command}`, errorMsg);
|
|
41
|
+
resolve({
|
|
42
|
+
content: [{ type: "text", text: `ERROR: ${errorMsg}. Output: ${output}` }],
|
|
43
|
+
isError: true,
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Truncate long outputs
|
|
48
|
+
const MAX_OUTPUT_LENGTH = 5000;
|
|
49
|
+
let truncatedOutput = output;
|
|
50
|
+
if (output.length > MAX_OUTPUT_LENGTH) {
|
|
51
|
+
truncatedOutput = output.substring(0, MAX_OUTPUT_LENGTH) + "... [TRUNCATED] ..."; // Simplified
|
|
52
|
+
}
|
|
53
|
+
resolve({ content: [{ type: "text", text: truncatedOutput }] });
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
/**
|
|
5
|
+
* Executes a command safely and returns the output.
|
|
6
|
+
*/
|
|
7
|
+
export function safeExec(cmd, args, cwd, timeout = 30000) {
|
|
8
|
+
try {
|
|
9
|
+
return execFileSync(cmd, args, { cwd, timeout, encoding: "utf8", stdio: "pipe" });
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
const error = err;
|
|
13
|
+
return error.stdout?.toString() || error.stderr?.toString() || error.message || String(err);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Detects the backend language from the framework configuration.
|
|
18
|
+
*/
|
|
19
|
+
export function getBackendLanguage(projectRoot) {
|
|
20
|
+
try {
|
|
21
|
+
const configPath = path.join(projectRoot, ".atabey", "config.json");
|
|
22
|
+
if (fs.existsSync(configPath)) {
|
|
23
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
24
|
+
return config.backendLanguage || "Node.js (TypeScript)";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Fallback to default
|
|
29
|
+
}
|
|
30
|
+
return "Node.js (TypeScript)";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns the default lint command for the given language.
|
|
34
|
+
*/
|
|
35
|
+
export function getDefaultLintCommand(language) {
|
|
36
|
+
if (language.includes("Go"))
|
|
37
|
+
return "go fmt ./...";
|
|
38
|
+
if (language.includes("Java"))
|
|
39
|
+
return "./gradlew check"; // or mvn check
|
|
40
|
+
if (language.includes("Python"))
|
|
41
|
+
return "ruff check .";
|
|
42
|
+
if (language.includes(".NET"))
|
|
43
|
+
return "dotnet format";
|
|
44
|
+
return "npm run lint";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Returns the default test command for the given language.
|
|
48
|
+
*/
|
|
49
|
+
export function getDefaultTestCommand(language) {
|
|
50
|
+
if (language.includes("Go"))
|
|
51
|
+
return "go test ./...";
|
|
52
|
+
if (language.includes("Java"))
|
|
53
|
+
return "./gradlew test"; // or mvn test
|
|
54
|
+
if (language.includes("Python"))
|
|
55
|
+
return "pytest";
|
|
56
|
+
if (language.includes(".NET"))
|
|
57
|
+
return "dotnet test";
|
|
58
|
+
return "npm test";
|
|
59
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
/**
|
|
3
|
+
* Enterprise Compliance Guardrail
|
|
4
|
+
* Checks content against corporate standards using AST analysis before allowing file mutations.
|
|
5
|
+
*/
|
|
6
|
+
export function verifyCorporateCompliance(content, filePath) {
|
|
7
|
+
// Skip compliance checks for non-source files or specific ignored files
|
|
8
|
+
if (filePath.endsWith(".json") || filePath.endsWith(".md") || filePath.endsWith(".env.example")) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
12
|
+
const errors = [];
|
|
13
|
+
/**
|
|
14
|
+
* Recursive AST Visitor
|
|
15
|
+
*/
|
|
16
|
+
function visit(node) {
|
|
17
|
+
// 1. Zero Console Policy
|
|
18
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
19
|
+
const expression = node.expression;
|
|
20
|
+
const name = node.name.text;
|
|
21
|
+
if (ts.isIdentifier(expression) && expression.text === "console") {
|
|
22
|
+
if (["log", "warn", "error"].includes(name)) {
|
|
23
|
+
// Check if file is exempt
|
|
24
|
+
if (!filePath.includes("logger.ts") && !filePath.includes("check.ts") && !filePath.includes("cli.ts")) {
|
|
25
|
+
errors.push(`❌ Corporate Compliance Breach: 'console.${name}' usage is forbidden at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// 2. No Explicit Any Policy
|
|
31
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
32
|
+
if (ts.isIdentifier(node.typeName) && node.typeName.text === "any") {
|
|
33
|
+
if (!filePath.includes("definitions.ts") && !filePath.includes("types.ts")) {
|
|
34
|
+
errors.push(`❌ Corporate Compliance Breach: 'any' type is forbidden at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// 3. Zero UI Library Policy (No @chakra-ui, mui, @shadcn)
|
|
39
|
+
if (ts.isImportDeclaration(node)) {
|
|
40
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
41
|
+
if (ts.isStringLiteral(moduleSpecifier)) {
|
|
42
|
+
const forbiddenLibs = ["@chakra-ui", "mui", "@shadcn", "antd", "bootstrap"];
|
|
43
|
+
const lib = forbiddenLibs.find(l => moduleSpecifier.text.includes(l));
|
|
44
|
+
if (lib) {
|
|
45
|
+
errors.push(`❌ Corporate Compliance Breach: External UI library '${lib}' usage is FORBIDDEN at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}. Build atomic components manually instead.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Handle 'any' as a keyword type (e.g., parameter: any)
|
|
50
|
+
if (node.kind === ts.SyntaxKind.AnyKeyword) {
|
|
51
|
+
if (!filePath.includes("definitions.ts") && !filePath.includes("types.ts")) {
|
|
52
|
+
errors.push(`❌ Corporate Compliance Breach: 'any' keyword is forbidden at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
ts.forEachChild(node, visit);
|
|
56
|
+
}
|
|
57
|
+
visit(sourceFile);
|
|
58
|
+
// 3. Hardcoded Secrets & PII Guard
|
|
59
|
+
const piiKeywords = [
|
|
60
|
+
{ regex: /API_KEY\s*=\s*['"][^'"]+['"]/i, msg: "Hardcoded API Key" },
|
|
61
|
+
{ regex: /SECRET\s*=\s*['"][^'"]+['"]/i, msg: "Hardcoded Secret" },
|
|
62
|
+
{ regex: /PASSWORD\s*=\s*['"][^'"]+['"]/i, msg: "Hardcoded Password" },
|
|
63
|
+
{ regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/, msg: "PII Detected: Email Address" },
|
|
64
|
+
{ regex: /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/, msg: "PII Detected: Credit Card Pattern" }
|
|
65
|
+
];
|
|
66
|
+
for (const { regex, msg } of piiKeywords) {
|
|
67
|
+
if (regex.test(content)) {
|
|
68
|
+
// Allow emails in specific files like README or package.json
|
|
69
|
+
if (msg.includes("Email") && (filePath.endsWith("README.md") || filePath.endsWith("package.json") || filePath.includes("CONTRIBUTING"))) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
errors.push(`❌ Corporate Compliance Breach: ${msg} detected.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (errors.length > 0) {
|
|
76
|
+
throw new Error(errors.join("\n"));
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/utils/fs.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Ensures directory existence.
|
|
5
|
+
*/
|
|
6
|
+
export function ensureDir(dirPath) {
|
|
7
|
+
if (!fs.existsSync(dirPath)) {
|
|
8
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Atomically writes a text file.
|
|
13
|
+
*/
|
|
14
|
+
export function writeTextFileAtomic(filePath, content) {
|
|
15
|
+
const dir = path.dirname(filePath);
|
|
16
|
+
ensureDir(dir);
|
|
17
|
+
const tempPath = `${filePath}.${Math.random().toString(36).slice(2, 9)}.tmp`;
|
|
18
|
+
const finalContent = content.endsWith("\n") ? content : `${content}\n`;
|
|
19
|
+
try {
|
|
20
|
+
fs.writeFileSync(tempPath, finalContent, "utf8");
|
|
21
|
+
fs.renameSync(tempPath, filePath);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
if (fs.existsSync(tempPath)) {
|
|
25
|
+
try {
|
|
26
|
+
fs.unlinkSync(tempPath);
|
|
27
|
+
}
|
|
28
|
+
catch { /* ignore */ }
|
|
29
|
+
}
|
|
30
|
+
throw err;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Atomically appends to a file (if supported by OS) or simulates it.
|
|
35
|
+
* Note: Real atomic append on POSIX is a single write() call with O_APPEND.
|
|
36
|
+
* For simplicity and robustness across platforms, we use a simple append here
|
|
37
|
+
* as the risk of corruption is lower than a full rewrite, but for logs
|
|
38
|
+
* it's acceptable.
|
|
39
|
+
*/
|
|
40
|
+
export function appendFileSafe(filePath, content) {
|
|
41
|
+
const dir = path.dirname(filePath);
|
|
42
|
+
ensureDir(dir);
|
|
43
|
+
fs.appendFileSync(filePath, content, "utf8");
|
|
44
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { resolveFrameworkDir } from "./security.js";
|
|
4
|
+
export const Metrics = {
|
|
5
|
+
/**
|
|
6
|
+
* Estimates tokens based on character count (rough heuristic: 1 token ~= 4 chars).
|
|
7
|
+
*/
|
|
8
|
+
estimateTokens: (text) => {
|
|
9
|
+
return Math.ceil(text.length / 4);
|
|
10
|
+
},
|
|
11
|
+
/**
|
|
12
|
+
* Logs the token usage and action to the observability metrics file.
|
|
13
|
+
*/
|
|
14
|
+
logUsage: (projectRoot, agent, action, tokens) => {
|
|
15
|
+
Metrics.saveMetric(projectRoot, {
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
agent,
|
|
18
|
+
action,
|
|
19
|
+
estimatedTokens: tokens
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
/**
|
|
23
|
+
* Logs an error occurrence to the observability metrics file.
|
|
24
|
+
*/
|
|
25
|
+
logError: (projectRoot, agent, action, error) => {
|
|
26
|
+
Metrics.saveMetric(projectRoot, {
|
|
27
|
+
timestamp: new Date().toISOString(),
|
|
28
|
+
agent,
|
|
29
|
+
action: `ERROR: ${action}`,
|
|
30
|
+
estimatedTokens: 0,
|
|
31
|
+
error
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* Internal helper to save metric entries.
|
|
36
|
+
*/
|
|
37
|
+
saveMetric: (projectRoot, entry) => {
|
|
38
|
+
const frameworkDir = resolveFrameworkDir(projectRoot);
|
|
39
|
+
const metricsPath = path.join(projectRoot, frameworkDir, "observability/metrics.json");
|
|
40
|
+
try {
|
|
41
|
+
const metricsDir = path.dirname(metricsPath);
|
|
42
|
+
if (!fs.existsSync(metricsDir))
|
|
43
|
+
fs.mkdirSync(metricsDir, { recursive: true });
|
|
44
|
+
let currentMetrics = [];
|
|
45
|
+
if (fs.existsSync(metricsPath)) {
|
|
46
|
+
currentMetrics = JSON.parse(fs.readFileSync(metricsPath, "utf8"));
|
|
47
|
+
}
|
|
48
|
+
currentMetrics.push(entry);
|
|
49
|
+
// Keep only last 100 entries to save space
|
|
50
|
+
if (currentMetrics.length > 100)
|
|
51
|
+
currentMetrics.shift();
|
|
52
|
+
fs.writeFileSync(metricsPath, JSON.stringify(currentMetrics, null, 2));
|
|
53
|
+
}
|
|
54
|
+
catch { /* ignore: metrics should not block the main process */ }
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { FRAMEWORK, MCP, UNIFIED_HUB_DIR } from "../constants.js"; // New import
|
|
4
|
+
import os from "os"; // Need os.homedir()
|
|
5
|
+
/**
|
|
6
|
+
* Validates and resolves a user-provided path to prevent path traversal attacks.
|
|
7
|
+
* Ensures the resolved path stays within the project root boundary.
|
|
8
|
+
*/
|
|
9
|
+
export function safePath(projectRoot, userPath) {
|
|
10
|
+
const resolved = path.resolve(projectRoot, userPath);
|
|
11
|
+
const normalizedRoot = path.resolve(projectRoot);
|
|
12
|
+
if (!resolved.startsWith(normalizedRoot + path.sep) && resolved !== normalizedRoot) {
|
|
13
|
+
throw new Error(`Access denied: path "${userPath}" escapes project root.`);
|
|
14
|
+
}
|
|
15
|
+
return resolved;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Resolves the active framework directory.
|
|
19
|
+
* Priority: ATABEY_TEST_DIR (env) -> package.json `atabey.frameworkDir` -> `.atabey` -> other adapter dirs -> global HOME.
|
|
20
|
+
*/
|
|
21
|
+
export function resolveFrameworkDir(projectRoot) {
|
|
22
|
+
// For test environments, use the explicitly set test directory.
|
|
23
|
+
const testDir = process.env[MCP.TEST_DIR_ENV];
|
|
24
|
+
if (testDir)
|
|
25
|
+
return testDir;
|
|
26
|
+
// 1. Authoritative source: read from package.json if present
|
|
27
|
+
try {
|
|
28
|
+
const pkgPath = path.join(projectRoot, "package.json");
|
|
29
|
+
if (fs.existsSync(pkgPath)) {
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
31
|
+
const atabeyConfig = pkg["atabey"];
|
|
32
|
+
if (atabeyConfig && typeof atabeyConfig["frameworkDir"] === "string") {
|
|
33
|
+
// Ensure the path is relative if it's within the project, otherwise use as-is.
|
|
34
|
+
const resolvedDir = path.resolve(projectRoot, atabeyConfig["frameworkDir"]);
|
|
35
|
+
if (resolvedDir.startsWith(path.resolve(projectRoot))) {
|
|
36
|
+
return path.relative(projectRoot, resolvedDir);
|
|
37
|
+
}
|
|
38
|
+
return atabeyConfig["frameworkDir"];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// ignore — fall through to filesystem scan
|
|
44
|
+
}
|
|
45
|
+
// 2. Filesystem scan in projectRoot for common framework directories
|
|
46
|
+
const localCandidates = [
|
|
47
|
+
FRAMEWORK.CORE_DIR, // .atabey
|
|
48
|
+
UNIFIED_HUB_DIR, // .agents
|
|
49
|
+
// Add other adapter specific directories if needed, or remove if unified is strictly enforced
|
|
50
|
+
];
|
|
51
|
+
for (const candidate of localCandidates) {
|
|
52
|
+
const candidatePath = path.join(projectRoot, candidate);
|
|
53
|
+
if (fs.existsSync(candidatePath)) {
|
|
54
|
+
return candidate;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// 3. Fallback to global home directory.
|
|
58
|
+
const homeDir = os.homedir();
|
|
59
|
+
return path.join(homeDir, FRAMEWORK.CORE_DIR);
|
|
60
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "atabey-mcp",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "Agent Atabey Model Context Protocol (MCP) Server",
|
|
5
|
+
"author": "Yusuf BEKAR",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ysf-bkr/atabey.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/ysf-bkr/atabey#readme",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "npx tsc",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"dev": "tsx src/index.ts"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
20
|
+
"zod": "^3.24.2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^25.9.1",
|
|
24
|
+
"typescript": "^5.9.3"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Atabey — Single Source of Truth for framework constants.
|
|
5
|
+
* Import from here instead of hardcoding paths, phases, or directory names.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─── Framework identity ───────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export const FRAMEWORK = {
|
|
11
|
+
NAME: "Agent Atabey",
|
|
12
|
+
CORE_DIR: ".atabey",
|
|
13
|
+
// This is the hub for unified adapter layouts (e.g. .agents/gemini, .agents/claude)
|
|
14
|
+
UNIFIED_HUB_DIR: ".agents",
|
|
15
|
+
// This is the default directory to scaffold new apps into
|
|
16
|
+
APPS_DIR: "apps",
|
|
17
|
+
// This is where all skills are stored
|
|
18
|
+
SKILLS_DIR: "skills",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const FRAMEWORK_SUBDIRS = {
|
|
22
|
+
AGENTS: "agents",
|
|
23
|
+
SKILLS: "skills",
|
|
24
|
+
KNOWLEDGE: "knowledge",
|
|
25
|
+
MESSAGES: "messages",
|
|
26
|
+
MEMORY: "memory",
|
|
27
|
+
MEMORY_GRAPH: "memory-graph",
|
|
28
|
+
LOGS: "logs",
|
|
29
|
+
CONFIG: "config",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const ROOT_CONFIG_FILES = {
|
|
33
|
+
MCP: "mcp.json",
|
|
34
|
+
NATIVE_MODULES: "native-modules.json",
|
|
35
|
+
TSCONFIG: "tsconfig.json",
|
|
36
|
+
ESLINT: "eslint.config.js",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const MCP = {
|
|
40
|
+
// Environment variable used by MCP to identify project root
|
|
41
|
+
PROJECT_ROOT_ENV: "ATABEY_PROJECT_ROOT",
|
|
42
|
+
// Environment variable for test mode
|
|
43
|
+
TEST_DIR_ENV: "ATABEY_TEST_DIR",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const MEMORY_FILES = {
|
|
47
|
+
STATE: "state.json",
|
|
48
|
+
SHARED_FACTS: "shared_facts.json",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const NATIVE_AGENT_PATHS = {
|
|
52
|
+
gemini: ".gemini/agents",
|
|
53
|
+
claude: ".claude/agents",
|
|
54
|
+
cursor: ".cursor/rules",
|
|
55
|
+
codex: ".agents/instructions",
|
|
56
|
+
grok: ".grok",
|
|
57
|
+
"antigravity-cli": ".antigravity/agents",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ─── Backward-compatible aliases ──────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export const CORE_FRAMEWORK_DIR = FRAMEWORK.CORE_DIR;
|
|
63
|
+
export const UNIFIED_HUB_DIR = FRAMEWORK.UNIFIED_HUB_DIR;
|
|
64
|
+
export const SKILLS_HUB_PATH = pathJoin(UNIFIED_HUB_DIR, FRAMEWORK_SUBDIRS.SKILLS);
|
|
65
|
+
|
|
66
|
+
// ─── Path Helpers ─────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function pathJoin(...args: string[]): string {
|
|
69
|
+
return path.join(...args);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function corePath(subdir: string, filename: string): string {
|
|
73
|
+
return pathJoin(FRAMEWORK.CORE_DIR, subdir, filename);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function knowledgePath(filename: string): string {
|
|
77
|
+
return corePath(FRAMEWORK_SUBDIRS.KNOWLEDGE, filename);
|
|
78
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare module "@modelcontextprotocol/sdk/server/index.js" {
|
|
2
|
+
export class Server {
|
|
3
|
+
constructor(info: { name: string; version: string }, options: { capabilities: { tools: Record<string, never> } });
|
|
4
|
+
setRequestHandler(schema: unknown, handler: (request: unknown) => Promise<unknown>): void;
|
|
5
|
+
connect(transport: unknown): Promise<void>;
|
|
6
|
+
close(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare module "@modelcontextprotocol/sdk/server/stdio.js" {
|
|
11
|
+
export class StdioServerTransport {}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare module "@modelcontextprotocol/sdk/types.js" {
|
|
15
|
+
export const ListToolsRequestSchema: unknown;
|
|
16
|
+
export const CallToolRequestSchema: unknown;
|
|
17
|
+
}
|