atabey 0.0.7 → 0.0.8
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/README.md +1 -1
- package/dist/src/cli/adapters/core.js +12 -19
- package/dist/src/cli/adapters/core.js.map +1 -1
- package/dist/src/shared/constants.d.ts +1 -0
- package/dist/src/shared/constants.js +1 -0
- package/dist/src/shared/constants.js.map +1 -1
- package/dist/tests/adapter.test.js +3 -2
- package/dist/tests/adapter.test.js.map +1 -1
- package/framework-mcp/dist/constants.js +64 -0
- package/framework-mcp/dist/framework-mcp/src/constants.js +64 -0
- package/framework-mcp/dist/framework-mcp/src/index.js +144 -0
- package/framework-mcp/dist/framework-mcp/src/resources/index.js +58 -0
- package/framework-mcp/dist/framework-mcp/src/tools/control_plane/locking.js +82 -0
- package/framework-mcp/dist/framework-mcp/src/tools/control_plane/registry.js +35 -0
- package/framework-mcp/dist/framework-mcp/src/tools/definitions.js +322 -0
- package/framework-mcp/dist/framework-mcp/src/tools/file_system/batch_surgical_edit.js +64 -0
- package/framework-mcp/dist/framework-mcp/src/tools/file_system/patch_file.js +34 -0
- package/framework-mcp/dist/framework-mcp/src/tools/file_system/read_file.js +51 -0
- package/framework-mcp/dist/framework-mcp/src/tools/file_system/replace_text.js +50 -0
- package/framework-mcp/dist/framework-mcp/src/tools/file_system/write_file.js +43 -0
- package/framework-mcp/dist/framework-mcp/src/tools/framework/audit_deps.js +41 -0
- package/framework-mcp/dist/framework-mcp/src/tools/framework/get_status.js +5 -0
- package/framework-mcp/dist/framework-mcp/src/tools/framework/orchestrate.js +5 -0
- package/framework-mcp/dist/framework-mcp/src/tools/framework/run_tests.js +27 -0
- package/framework-mcp/dist/framework-mcp/src/tools/framework/submit_plan.js +13 -0
- package/framework-mcp/dist/framework-mcp/src/tools/framework/update_contract_hash.js +5 -0
- package/framework-mcp/dist/framework-mcp/src/tools/framework/update_memory.js +8 -0
- package/framework-mcp/dist/framework-mcp/src/tools/index.js +62 -0
- package/framework-mcp/dist/framework-mcp/src/tools/memory/get_insights.js +34 -0
- package/framework-mcp/dist/framework-mcp/src/tools/memory/read_memory.js +28 -0
- package/framework-mcp/dist/framework-mcp/src/tools/messaging/log_action.js +22 -0
- package/framework-mcp/dist/framework-mcp/src/tools/messaging/send_message.js +94 -0
- package/framework-mcp/dist/framework-mcp/src/tools/observability/check_ports.js +26 -0
- package/framework-mcp/dist/framework-mcp/src/tools/observability/get_health.js +19 -0
- package/framework-mcp/dist/framework-mcp/src/tools/quality/check_lint.js +30 -0
- package/framework-mcp/dist/framework-mcp/src/tools/search/get_gaps.js +48 -0
- package/framework-mcp/dist/framework-mcp/src/tools/search/get_map.js +43 -0
- package/framework-mcp/dist/framework-mcp/src/tools/search/grep_search.js +75 -0
- package/framework-mcp/dist/framework-mcp/src/tools/search/list_dir.js +28 -0
- package/framework-mcp/dist/framework-mcp/src/tools/shell/run_command.js +56 -0
- package/framework-mcp/dist/framework-mcp/src/tools/types.js +1 -0
- package/framework-mcp/dist/framework-mcp/src/utils/cli.js +59 -0
- package/framework-mcp/dist/framework-mcp/src/utils/compliance.js +231 -0
- package/framework-mcp/dist/framework-mcp/src/utils/fs.js +44 -0
- package/framework-mcp/dist/framework-mcp/src/utils/metrics.js +56 -0
- package/framework-mcp/dist/framework-mcp/src/utils/permissions.js +71 -0
- package/framework-mcp/dist/framework-mcp/src/utils/security.js +60 -0
- package/framework-mcp/dist/index.js +144 -0
- package/framework-mcp/dist/resources/index.js +58 -0
- package/framework-mcp/dist/src/cli/adapters/core.js +71 -0
- package/framework-mcp/dist/src/cli/adapters/index.js +5 -0
- package/framework-mcp/dist/src/cli/adapters/paths.js +101 -0
- package/framework-mcp/dist/src/cli/adapters/scaffold.js +71 -0
- package/framework-mcp/dist/src/cli/adapters/utils.js +75 -0
- package/framework-mcp/dist/src/cli/commands/approve.js +63 -0
- package/framework-mcp/dist/src/cli/commands/check.js +181 -0
- package/framework-mcp/dist/src/cli/commands/compliance.js +50 -0
- package/framework-mcp/dist/src/cli/commands/contract.js +50 -0
- package/framework-mcp/dist/src/cli/commands/dashboard.js +123 -0
- package/framework-mcp/dist/src/cli/commands/explorer.js +42 -0
- package/framework-mcp/dist/src/cli/commands/git.js +40 -0
- package/framework-mcp/dist/src/cli/commands/init/create-agent.js +58 -0
- package/framework-mcp/dist/src/cli/commands/init/scaffold-core.js +112 -0
- package/framework-mcp/dist/src/cli/commands/init/scaffold-docs.js +34 -0
- package/framework-mcp/dist/src/cli/commands/init/scaffold-ops.js +80 -0
- package/framework-mcp/dist/src/cli/commands/init/scaffold-standards.js +67 -0
- package/framework-mcp/dist/src/cli/commands/init.js +167 -0
- package/framework-mcp/dist/src/cli/commands/knowledge.js +42 -0
- package/framework-mcp/dist/src/cli/commands/lint.js +22 -0
- package/framework-mcp/dist/src/cli/commands/log.js +10 -0
- package/framework-mcp/dist/src/cli/commands/memory.js +4 -0
- package/framework-mcp/dist/src/cli/commands/orchestrate.js +159 -0
- package/framework-mcp/dist/src/cli/commands/plan.js +117 -0
- package/framework-mcp/dist/src/cli/commands/script.js +19 -0
- package/framework-mcp/dist/src/cli/commands/security.js +36 -0
- package/framework-mcp/dist/src/cli/commands/status.js +97 -0
- package/framework-mcp/dist/src/cli/commands/trace.js +109 -0
- package/framework-mcp/dist/src/cli/index.js +338 -0
- package/framework-mcp/dist/src/cli/shims.js +66 -0
- package/framework-mcp/dist/src/cli/utils/claude.js +56 -0
- package/framework-mcp/dist/src/cli/utils/compliance.js +173 -0
- package/framework-mcp/dist/src/cli/utils/config-schema.js +42 -0
- package/framework-mcp/dist/src/cli/utils/fs.js +137 -0
- package/framework-mcp/dist/src/cli/utils/i18n.js +30 -0
- package/framework-mcp/dist/src/cli/utils/memory.js +276 -0
- package/framework-mcp/dist/src/cli/utils/pkg.js +282 -0
- package/framework-mcp/dist/src/cli/utils/schemas.js +19 -0
- package/framework-mcp/dist/src/cli/utils/string.js +49 -0
- package/framework-mcp/dist/src/cli/utils/time.js +27 -0
- package/framework-mcp/dist/src/cli/utils/ui.js +58 -0
- package/framework-mcp/dist/src/contracts/index.js +1 -0
- package/framework-mcp/dist/src/contracts/tasks.js +20 -0
- package/framework-mcp/dist/src/dashboard/vite.config.js +15 -0
- package/framework-mcp/dist/src/modules/adapters/definitions.js +140 -0
- package/framework-mcp/dist/src/modules/adapters/registry.js +18 -0
- package/framework-mcp/dist/src/modules/adapters/shared.js +104 -0
- package/framework-mcp/dist/src/modules/adapters/types.js +1 -0
- package/framework-mcp/dist/src/modules/agents/definitions.js +457 -0
- package/framework-mcp/dist/src/modules/agents/registry/analyst.js +39 -0
- package/framework-mcp/dist/src/modules/agents/registry/architect.js +42 -0
- package/framework-mcp/dist/src/modules/agents/registry/backend.js +49 -0
- package/framework-mcp/dist/src/modules/agents/registry/database.js +45 -0
- package/framework-mcp/dist/src/modules/agents/registry/devops.js +45 -0
- package/framework-mcp/dist/src/modules/agents/registry/explorer.js +36 -0
- package/framework-mcp/dist/src/modules/agents/registry/frontend.js +51 -0
- package/framework-mcp/dist/src/modules/agents/registry/git.js +36 -0
- package/framework-mcp/dist/src/modules/agents/registry/manager.js +53 -0
- package/framework-mcp/dist/src/modules/agents/registry/mobile.js +39 -0
- package/framework-mcp/dist/src/modules/agents/registry/native.js +39 -0
- package/framework-mcp/dist/src/modules/agents/registry/quality.js +41 -0
- package/framework-mcp/dist/src/modules/agents/registry/security.js +43 -0
- package/framework-mcp/dist/src/modules/agents/types.js +1 -0
- package/framework-mcp/dist/src/modules/engines/evaluation-engine.js +102 -0
- package/framework-mcp/dist/src/modules/engines/health-engine.js +49 -0
- package/framework-mcp/dist/src/modules/engines/planning-engine.js +78 -0
- package/framework-mcp/dist/src/modules/engines/risk-engine.js +105 -0
- package/framework-mcp/dist/src/modules/engines/routing-engine.js +73 -0
- package/framework-mcp/dist/src/modules/engines/types.js +1 -0
- package/framework-mcp/dist/src/modules/skills/definitions.js +70 -0
- package/framework-mcp/dist/src/shared/constants.js +187 -0
- package/framework-mcp/dist/src/shared/errors.js +68 -0
- package/framework-mcp/dist/src/shared/fs.js +51 -0
- package/framework-mcp/dist/src/shared/logger.js +116 -0
- package/framework-mcp/dist/src/shared/storage.js +207 -0
- package/framework-mcp/dist/src/shared/types.js +12 -0
- package/framework-mcp/dist/tools/control_plane/locking.js +82 -0
- package/framework-mcp/dist/tools/control_plane/registry.js +35 -0
- package/framework-mcp/dist/tools/definitions.js +322 -0
- package/framework-mcp/dist/tools/file_system/batch_surgical_edit.js +64 -0
- package/framework-mcp/dist/tools/file_system/patch_file.js +34 -0
- package/framework-mcp/dist/tools/file_system/read_file.js +51 -0
- package/framework-mcp/dist/tools/file_system/replace_text.js +50 -0
- package/framework-mcp/dist/tools/file_system/write_file.js +43 -0
- package/framework-mcp/dist/tools/framework/audit_deps.js +41 -0
- package/framework-mcp/dist/tools/framework/get_status.js +5 -0
- package/framework-mcp/dist/tools/framework/orchestrate.js +5 -0
- package/framework-mcp/dist/tools/framework/run_tests.js +27 -0
- package/framework-mcp/dist/tools/framework/submit_plan.js +13 -0
- package/framework-mcp/dist/tools/framework/update_contract_hash.js +5 -0
- package/framework-mcp/dist/tools/framework/update_memory.js +8 -0
- package/framework-mcp/dist/tools/index.js +62 -0
- package/framework-mcp/dist/tools/memory/get_insights.js +34 -0
- package/framework-mcp/dist/tools/memory/read_memory.js +28 -0
- package/framework-mcp/dist/tools/messaging/log_action.js +22 -0
- package/framework-mcp/dist/tools/messaging/send_message.js +94 -0
- package/framework-mcp/dist/tools/observability/check_ports.js +26 -0
- package/framework-mcp/dist/tools/observability/get_health.js +19 -0
- package/framework-mcp/dist/tools/quality/check_lint.js +30 -0
- package/framework-mcp/dist/tools/search/get_gaps.js +48 -0
- package/framework-mcp/dist/tools/search/get_map.js +43 -0
- package/framework-mcp/dist/tools/search/grep_search.js +75 -0
- package/framework-mcp/dist/tools/search/list_dir.js +28 -0
- package/framework-mcp/dist/tools/shell/run_command.js +56 -0
- package/framework-mcp/dist/tools/types.js +1 -0
- package/framework-mcp/dist/utils/cli.js +59 -0
- package/framework-mcp/dist/utils/compliance.js +231 -0
- package/framework-mcp/dist/utils/fs.js +44 -0
- package/framework-mcp/dist/utils/metrics.js +56 -0
- package/framework-mcp/dist/utils/permissions.js +71 -0
- package/framework-mcp/dist/utils/security.js +60 -0
- package/framework-mcp/package.json +35 -0
- package/mcp.json +1 -1
- package/package.json +6 -3
|
@@ -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,231 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { resolveFrameworkDir } from "./security.js";
|
|
5
|
+
/**
|
|
6
|
+
* Enterprise Compliance Guardrail
|
|
7
|
+
* Checks content against corporate standards using AST analysis before allowing file mutations.
|
|
8
|
+
*/
|
|
9
|
+
export function verifyCorporateCompliance(content, filePath) {
|
|
10
|
+
// Skip compliance checks for non-source files or specific ignored files
|
|
11
|
+
if (filePath.endsWith(".json") || filePath.endsWith(".md") || filePath.endsWith(".env.example")) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
15
|
+
const errors = [];
|
|
16
|
+
/**
|
|
17
|
+
* Recursive AST Visitor
|
|
18
|
+
*/
|
|
19
|
+
function visit(node) {
|
|
20
|
+
// 1. Zero Console Policy
|
|
21
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
22
|
+
const expression = node.expression;
|
|
23
|
+
const name = node.name.text;
|
|
24
|
+
if (ts.isIdentifier(expression) && expression.text === "console") {
|
|
25
|
+
if (["log", "warn", "error"].includes(name)) {
|
|
26
|
+
// Check if file is exempt
|
|
27
|
+
if (!filePath.includes("logger.ts") && !filePath.includes("check.ts") && !filePath.includes("cli.ts")) {
|
|
28
|
+
errors.push(`[ERROR] Corporate Compliance Breach: 'console.${name}' usage is forbidden at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// 2. No Explicit Any Policy
|
|
34
|
+
if (ts.isTypeReferenceNode(node)) {
|
|
35
|
+
if (ts.isIdentifier(node.typeName) && node.typeName.text === "any") {
|
|
36
|
+
if (!filePath.includes("definitions.ts") && !filePath.includes("types.ts")) {
|
|
37
|
+
errors.push(`[ERROR] Corporate Compliance Breach: 'any' type is forbidden at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// 3. Zero UI Library Policy (No @chakra-ui, mui, @shadcn)
|
|
42
|
+
if (ts.isImportDeclaration(node)) {
|
|
43
|
+
const moduleSpecifier = node.moduleSpecifier;
|
|
44
|
+
if (ts.isStringLiteral(moduleSpecifier)) {
|
|
45
|
+
const forbiddenLibs = ["@chakra-ui", "mui", "@shadcn", "antd", "bootstrap"];
|
|
46
|
+
const lib = forbiddenLibs.find(l => moduleSpecifier.text.includes(l));
|
|
47
|
+
if (lib) {
|
|
48
|
+
errors.push(`[ERROR] Corporate Compliance Breach: External UI library '${lib}' usage is FORBIDDEN at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}. Build atomic components manually instead.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Handle 'any' as a keyword type (e.g., parameter: any)
|
|
53
|
+
if (node.kind === ts.SyntaxKind.AnyKeyword) {
|
|
54
|
+
if (!filePath.includes("definitions.ts") && !filePath.includes("types.ts")) {
|
|
55
|
+
errors.push(`[ERROR] Corporate Compliance Breach: 'any' keyword is forbidden at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
ts.forEachChild(node, visit);
|
|
59
|
+
}
|
|
60
|
+
visit(sourceFile);
|
|
61
|
+
// 3. Hardcoded Secrets & PII Guard
|
|
62
|
+
const piiKeywords = [
|
|
63
|
+
{ regex: /API_KEY\s*=\s*['"][^'"]+['"]/i, msg: "Hardcoded API Key" },
|
|
64
|
+
{ regex: /SECRET\s*=\s*['"][^'"]+['"]/i, msg: "Hardcoded Secret" },
|
|
65
|
+
{ regex: /PASSWORD\s*=\s*['"][^'"]+['"]/i, msg: "Hardcoded Password" },
|
|
66
|
+
{ regex: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/, msg: "PII Detected: Email Address" },
|
|
67
|
+
{ regex: /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/, msg: "PII Detected: Credit Card Pattern" }
|
|
68
|
+
];
|
|
69
|
+
for (const { regex, msg } of piiKeywords) {
|
|
70
|
+
if (regex.test(content)) {
|
|
71
|
+
// Allow emails in specific files like README or package.json
|
|
72
|
+
if (msg.includes("Email") && (filePath.endsWith("README.md") || filePath.endsWith("package.json") || filePath.includes("CONTRIBUTING"))) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
errors.push(`[ERROR] Corporate Compliance Breach: ${msg} detected.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (errors.length > 0) {
|
|
79
|
+
throw new Error(errors.join("\n"));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function isHighRiskOperation(content, filePath) {
|
|
83
|
+
const fileName = filePath.toLowerCase();
|
|
84
|
+
// 1. Database Deletions / Table Drops
|
|
85
|
+
if (fileName.endsWith(".sql") || fileName.endsWith(".ts") || fileName.endsWith(".js") || fileName.endsWith(".go")) {
|
|
86
|
+
const dropRegex = /\b(DROP\s+(DATABASE|TABLE|SCHEMA|VIEW|INDEX)|TRUNCATE\s+TABLE)\b/i;
|
|
87
|
+
if (dropRegex.test(content)) {
|
|
88
|
+
return { isRisk: true, reason: "Database structural deletion detected (DROP/TRUNCATE)" };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// 2. Package Updates
|
|
92
|
+
if (fileName.endsWith("package.json")) {
|
|
93
|
+
return { isRisk: true, reason: "Dependency/package update operation detected" };
|
|
94
|
+
}
|
|
95
|
+
// 3. Deployment / Infrastructure Scripts
|
|
96
|
+
if (fileName.includes("deploy") ||
|
|
97
|
+
fileName.includes("dockerfile") ||
|
|
98
|
+
fileName.includes("docker-compose") ||
|
|
99
|
+
fileName.includes("k8s") ||
|
|
100
|
+
fileName.includes("kubernetes") ||
|
|
101
|
+
fileName.includes("github/workflows")) {
|
|
102
|
+
return { isRisk: true, reason: "Infrastructure or deployment script mutation detected" };
|
|
103
|
+
}
|
|
104
|
+
return { isRisk: false };
|
|
105
|
+
}
|
|
106
|
+
export async function verifyRiskAndAwaitApproval(projectRoot, content, filePath) {
|
|
107
|
+
const assessment = isHighRiskOperation(content, filePath);
|
|
108
|
+
if (!assessment.isRisk) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const frameworkDir = resolveFrameworkDir(projectRoot);
|
|
112
|
+
const absoluteFrameworkPath = path.isAbsolute(frameworkDir)
|
|
113
|
+
? frameworkDir
|
|
114
|
+
: path.resolve(projectRoot, frameworkDir);
|
|
115
|
+
const statusPath = path.join(absoluteFrameworkPath, "memory", "status.json");
|
|
116
|
+
const statePath = path.join(absoluteFrameworkPath, "memory", "state.json");
|
|
117
|
+
const messagesDir = path.join(absoluteFrameworkPath, "messages");
|
|
118
|
+
const managerMsgPath = path.join(messagesDir, "manager.json");
|
|
119
|
+
let activeAgent = null;
|
|
120
|
+
let traceId = "UNKNOWN";
|
|
121
|
+
// 1. Resolve traceId from state.json
|
|
122
|
+
if (fs.existsSync(statePath)) {
|
|
123
|
+
try {
|
|
124
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
125
|
+
if (state && state.traceId) {
|
|
126
|
+
traceId = state.traceId;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch { /* ignore */ }
|
|
130
|
+
}
|
|
131
|
+
// 2. Resolve active agent from status.json
|
|
132
|
+
let statusData = {};
|
|
133
|
+
if (fs.existsSync(statusPath)) {
|
|
134
|
+
try {
|
|
135
|
+
statusData = JSON.parse(fs.readFileSync(statusPath, "utf8"));
|
|
136
|
+
for (const [agentName, info] of Object.entries(statusData)) {
|
|
137
|
+
if (info.state === "EXECUTING") {
|
|
138
|
+
activeAgent = agentName.startsWith("@") ? agentName : `@${agentName}`;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch { /* ignore */ }
|
|
144
|
+
}
|
|
145
|
+
if (!activeAgent) {
|
|
146
|
+
throw new Error(`Security Exception: High-risk operation blocked. ${assessment.reason}. (No active executing agent found)`);
|
|
147
|
+
}
|
|
148
|
+
// 3. Update active agent status to WAITING_FOR_APPROVAL
|
|
149
|
+
const originalTask = statusData[activeAgent.replace("@", "")]?.task || statusData[activeAgent]?.task || "Executing task";
|
|
150
|
+
const statusKey = activeAgent.replace("@", "");
|
|
151
|
+
statusData[statusKey] = {
|
|
152
|
+
state: "WAITING_FOR_APPROVAL",
|
|
153
|
+
task: `[PAUSED] Waiting for approval: ${assessment.reason} on ${filePath}`
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2));
|
|
157
|
+
}
|
|
158
|
+
catch { /* ignore */ }
|
|
159
|
+
// 4. Create and append ALERT message to messages/manager.json
|
|
160
|
+
if (!fs.existsSync(messagesDir)) {
|
|
161
|
+
fs.mkdirSync(messagesDir, { recursive: true });
|
|
162
|
+
}
|
|
163
|
+
const alertMsg = {
|
|
164
|
+
timestamp: new Date().toISOString(),
|
|
165
|
+
from: activeAgent,
|
|
166
|
+
to: "@manager",
|
|
167
|
+
category: "ALERT",
|
|
168
|
+
content: `High-risk operation: ${assessment.reason} on ${filePath}`,
|
|
169
|
+
traceId: traceId,
|
|
170
|
+
status: "PENDING",
|
|
171
|
+
priority: "HIGH",
|
|
172
|
+
requiresApproval: true,
|
|
173
|
+
action: `MUTATION:${filePath}`
|
|
174
|
+
};
|
|
175
|
+
try {
|
|
176
|
+
fs.appendFileSync(managerMsgPath, JSON.stringify(alertMsg) + "\n");
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
statusData[statusKey] = { state: "EXECUTING", task: originalTask };
|
|
180
|
+
fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2));
|
|
181
|
+
throw new Error(`Security Exception: Failed to queue approval request. ${String(err)}`, { cause: err });
|
|
182
|
+
}
|
|
183
|
+
// 5. Polling Loop: Wait for approval (up to 60 seconds)
|
|
184
|
+
const pollIntervalMs = 500;
|
|
185
|
+
const timeoutMs = 60000;
|
|
186
|
+
const start = Date.now();
|
|
187
|
+
while (Date.now() - start < timeoutMs) {
|
|
188
|
+
// Non-blocking wait
|
|
189
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
190
|
+
if (fs.existsSync(managerMsgPath)) {
|
|
191
|
+
try {
|
|
192
|
+
const contentStr = fs.readFileSync(managerMsgPath, "utf8").trim();
|
|
193
|
+
const lines = contentStr.split("\n");
|
|
194
|
+
let isApproved = false;
|
|
195
|
+
let isDenied = false;
|
|
196
|
+
for (const line of lines) {
|
|
197
|
+
if (!line.trim())
|
|
198
|
+
continue;
|
|
199
|
+
const parsed = JSON.parse(line);
|
|
200
|
+
if (parsed.traceId === traceId && parsed.category === "ALERT" && parsed.action === `MUTATION:${filePath}`) {
|
|
201
|
+
if (parsed.status === "APPROVED") {
|
|
202
|
+
isApproved = true;
|
|
203
|
+
}
|
|
204
|
+
else if (parsed.status === "PROCESSED" || parsed.status === "DENIED") {
|
|
205
|
+
isDenied = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (isApproved) {
|
|
210
|
+
statusData[statusKey] = { state: "EXECUTING", task: originalTask };
|
|
211
|
+
fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (isDenied) {
|
|
215
|
+
throw new Error("Security Exception: High-risk operation was explicitly DENIED by user.");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
if (err.message.includes("explicitly DENIED")) {
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
statusData[statusKey] = { state: "EXECUTING", task: originalTask };
|
|
226
|
+
try {
|
|
227
|
+
fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2));
|
|
228
|
+
}
|
|
229
|
+
catch { /* ignore */ }
|
|
230
|
+
throw new Error(`Security Exception: High-risk operation timed out waiting for approval. (${assessment.reason})`);
|
|
231
|
+
}
|
|
@@ -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,71 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { resolveFrameworkDir } from "./security.js";
|
|
4
|
+
function globToRegex(glob) {
|
|
5
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
6
|
+
const step1 = escaped.replace(/\*\*/g, "__DBL_STR__");
|
|
7
|
+
const step2 = step1.replace(/\*/g, "[^/]*");
|
|
8
|
+
const regexStr = "^" + step2.replace(/__DBL_STR__/g, ".*") + "$";
|
|
9
|
+
return new RegExp(regexStr);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Validates if the active agent has write permission for the target file.
|
|
13
|
+
* Automatically identifies the active agent by checking the status.json store
|
|
14
|
+
* for the agent in the "EXECUTING" state.
|
|
15
|
+
*/
|
|
16
|
+
export function verifyWritePermission(projectRoot, targetFilePath) {
|
|
17
|
+
const frameworkDir = resolveFrameworkDir(projectRoot);
|
|
18
|
+
const absoluteFrameworkPath = path.isAbsolute(frameworkDir)
|
|
19
|
+
? frameworkDir
|
|
20
|
+
: path.resolve(projectRoot, frameworkDir);
|
|
21
|
+
const matrixPath = path.join(absoluteFrameworkPath, "permission-matrix.json");
|
|
22
|
+
// If no permission matrix exists, skip enforcement (default allow)
|
|
23
|
+
if (!fs.existsSync(matrixPath)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let matrix;
|
|
27
|
+
try {
|
|
28
|
+
matrix = JSON.parse(fs.readFileSync(matrixPath, "utf8"));
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
throw new Error(`Failed to parse permission-matrix.json: ${String(e)}`, { cause: e });
|
|
32
|
+
}
|
|
33
|
+
// Determine the active agent from status.json
|
|
34
|
+
const statusPath = path.join(absoluteFrameworkPath, "memory", "status.json");
|
|
35
|
+
let activeAgent = null;
|
|
36
|
+
if (fs.existsSync(statusPath)) {
|
|
37
|
+
try {
|
|
38
|
+
const status = JSON.parse(fs.readFileSync(statusPath, "utf8"));
|
|
39
|
+
// Find an agent that is currently in the EXECUTING state
|
|
40
|
+
for (const [agentName, info] of Object.entries(status)) {
|
|
41
|
+
const data = info;
|
|
42
|
+
if (data.state === "EXECUTING") {
|
|
43
|
+
activeAgent = agentName.startsWith("@") ? agentName : `@${agentName}`;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
// Log warning but don't crash, default to allowing if status can't be parsed
|
|
50
|
+
process.stderr.write(`[Permissions] Warning: Failed to read status.json: ${String(e)}\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// If no active executing agent is found, default to allowing
|
|
54
|
+
if (!activeAgent) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const agentRules = matrix[activeAgent];
|
|
58
|
+
// If no rules defined for the agent, default to allowing
|
|
59
|
+
if (!agentRules || !agentRules.write) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Resolve target path relative to project root for glob matching
|
|
63
|
+
const relativeTargetPath = path.relative(projectRoot, path.resolve(projectRoot, targetFilePath));
|
|
64
|
+
const allowed = agentRules.write.some(glob => {
|
|
65
|
+
const regex = globToRegex(glob);
|
|
66
|
+
return regex.test(relativeTargetPath);
|
|
67
|
+
});
|
|
68
|
+
if (!allowed) {
|
|
69
|
+
throw new Error(`Permission Denied: Agent ${activeAgent} is not authorized to write to "${relativeTargetPath}". Matrix rules restrict writes to: [${agentRules.write.join(", ")}].`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { TOOLS, toolHandlers } from "./tools/index.js";
|
|
9
|
+
import { RESOURCES, handleReadResource } from "./resources/index.js";
|
|
10
|
+
// ─── Server Setup ─────────────────────────────────────────────────
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
// Robustly find package.json by walking up from __dirname
|
|
13
|
+
function findPackageJson(startDir) {
|
|
14
|
+
let currentDir = startDir;
|
|
15
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
16
|
+
const pkgPath = path.join(currentDir, "package.json");
|
|
17
|
+
if (fs.existsSync(pkgPath))
|
|
18
|
+
return pkgPath;
|
|
19
|
+
currentDir = path.dirname(currentDir);
|
|
20
|
+
}
|
|
21
|
+
throw new Error("Could not find package.json for atabey-mcp");
|
|
22
|
+
}
|
|
23
|
+
const pkgPath = findPackageJson(__dirname);
|
|
24
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
25
|
+
const serverVersion = pkg.version;
|
|
26
|
+
const server = new Server({
|
|
27
|
+
name: "atabey-mcp",
|
|
28
|
+
version: serverVersion,
|
|
29
|
+
}, {
|
|
30
|
+
capabilities: {
|
|
31
|
+
tools: {},
|
|
32
|
+
resources: {},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
// Basic Schema Validator for Required Fields
|
|
36
|
+
function validateArgs(toolName, args) {
|
|
37
|
+
const definition = TOOLS.find(t => t.name === toolName);
|
|
38
|
+
if (!definition)
|
|
39
|
+
return `Unknown tool: ${toolName}`;
|
|
40
|
+
const required = definition.inputSchema.required || [];
|
|
41
|
+
for (const field of required) {
|
|
42
|
+
if (args[field] === undefined || args[field] === null || args[field] === "") {
|
|
43
|
+
return `Missing required argument: '${field}' for tool '${toolName}'`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
server.setRequestHandler(ListToolsRequestSchema, async (request) => {
|
|
49
|
+
// 2026 Stateless Spec: Log client info from metadata if available
|
|
50
|
+
const meta = request._meta;
|
|
51
|
+
if (meta) {
|
|
52
|
+
process.stderr.write(`[MCP] Stateless ListTools from ${meta.client?.name || "unknown"} v${meta.client?.version || "?.?"}\n`);
|
|
53
|
+
}
|
|
54
|
+
return { tools: TOOLS };
|
|
55
|
+
});
|
|
56
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
57
|
+
return { resources: RESOURCES };
|
|
58
|
+
});
|
|
59
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
60
|
+
const uri = request.params.uri;
|
|
61
|
+
try {
|
|
62
|
+
const content = await handleReadResource(uri);
|
|
63
|
+
return {
|
|
64
|
+
contents: [
|
|
65
|
+
{
|
|
66
|
+
uri,
|
|
67
|
+
mimeType: "text/markdown",
|
|
68
|
+
text: content,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
throw new Error(`Failed to read resource: ${message}`, { cause: error });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
79
|
+
const req = request;
|
|
80
|
+
const { name, arguments: args } = req.params;
|
|
81
|
+
const meta = request._meta;
|
|
82
|
+
// 2026 Stateless Spec: Prioritize metadata-driven context
|
|
83
|
+
if (meta) {
|
|
84
|
+
process.stderr.write(`[MCP] Stateless CallTool: ${name} (Client: ${meta.client?.name || "unknown"})\n`);
|
|
85
|
+
}
|
|
86
|
+
const projectRoot = process.env.ATABEY_PROJECT_ROOT || process.cwd();
|
|
87
|
+
try {
|
|
88
|
+
const handler = toolHandlers[name];
|
|
89
|
+
if (!handler) {
|
|
90
|
+
return {
|
|
91
|
+
isError: true,
|
|
92
|
+
content: [{ type: "text", text: `[ERROR] Unknown tool: ${name}` }],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// [SECURITY] Runtime Validation
|
|
96
|
+
const validationError = validateArgs(name, args || {});
|
|
97
|
+
if (validationError) {
|
|
98
|
+
return {
|
|
99
|
+
isError: true,
|
|
100
|
+
content: [{ type: "text", text: `[ERROR] Validation Error: ${validationError}` }],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return await handler(projectRoot, args || {});
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
107
|
+
return {
|
|
108
|
+
isError: true,
|
|
109
|
+
content: [{ type: "text", text: `[ERROR] Execution failed: ${message}` }],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
// ─── Graceful Startup & Shutdown ──────────────────────────────────
|
|
114
|
+
async function run() {
|
|
115
|
+
const transport = new StdioServerTransport();
|
|
116
|
+
// Prevent unhandled errors from crashing the MCP stream
|
|
117
|
+
process.on("uncaughtException", (error) => {
|
|
118
|
+
process.stderr.write(`[atabey-mcp] Uncaught exception: ${error.message}
|
|
119
|
+
`);
|
|
120
|
+
});
|
|
121
|
+
process.on("unhandledRejection", (reason) => {
|
|
122
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
123
|
+
process.stderr.write(`[atabey-mcp] Unhandled rejection: ${message}
|
|
124
|
+
`);
|
|
125
|
+
});
|
|
126
|
+
// Graceful shutdown on SIGINT/SIGTERM
|
|
127
|
+
const shutdown = async () => {
|
|
128
|
+
try {
|
|
129
|
+
await server.close();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Already closed or failed — safe to ignore
|
|
133
|
+
}
|
|
134
|
+
process.exit(0);
|
|
135
|
+
};
|
|
136
|
+
process.on("SIGINT", shutdown);
|
|
137
|
+
process.on("SIGTERM", shutdown);
|
|
138
|
+
await server.connect(transport);
|
|
139
|
+
}
|
|
140
|
+
run().catch((error) => {
|
|
141
|
+
process.stderr.write(`[atabey-mcp] Fatal startup error: ${error.message}
|
|
142
|
+
`);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
});
|