@agilsee/mcp-orchestrator 0.5.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.
- package/bin/cli.js +490 -0
- package/dist/index.js +454 -0
- package/dist/memory/memory-manager.js +234 -0
- package/dist/server/web-server.js +574 -0
- package/dist/tools/aggregate-patterns.js +101 -0
- package/dist/tools/analyze-history.js +213 -0
- package/dist/tools/auto-dispatch.js +199 -0
- package/dist/tools/check-energy.js +49 -0
- package/dist/tools/cross-search.js +171 -0
- package/dist/tools/get-focus.js +7 -0
- package/dist/tools/get-identity.js +7 -0
- package/dist/tools/get-project-status.js +35 -0
- package/dist/tools/list-projects.js +21 -0
- package/dist/tools/list-recent-tasks.js +59 -0
- package/dist/tools/log-insight.js +43 -0
- package/dist/tools/qcc-create.js +82 -0
- package/dist/tools/qcc-status.js +164 -0
- package/dist/tools/qcc-update.js +188 -0
- package/dist/tools/smart-bootstrap.js +255 -0
- package/dist/tools/summarize-session.js +161 -0
- package/dist/tools/switch-focus.js +40 -0
- package/dist/tools/workflow-router.js +438 -0
- package/package.json +44 -0
- package/templates/index.ts.template +42 -0
- package/templates/shared/get-claude-md.ts +12 -0
- package/templates/shared/get-current-state.ts +21 -0
- package/templates/shared/get-mistakes.ts +18 -0
- package/templates/shared/log-task.ts +27 -0
- package/templates/shared/predict-impact.ts +67 -0
- package/templates/shared/record-mistake.ts +40 -0
- package/templates/shared/update-state.ts +83 -0
- package/templates/stacks/express/config.json +9 -0
- package/templates/stacks/express/list-routes.ts +56 -0
- package/templates/stacks/express/symbol-index.ts +70 -0
- package/templates/stacks/laravel/config.json +9 -0
- package/templates/stacks/laravel/list-routes.ts +19 -0
- package/templates/stacks/laravel/symbol-index.ts +64 -0
- package/templates/stacks/nextjs/config.json +9 -0
- package/templates/stacks/nextjs/list-routes.ts +67 -0
- package/templates/stacks/nextjs/symbol-index.ts +78 -0
- package/templates/stacks/react/config.json +10 -0
- package/templates/stacks/react/list-routes.ts +44 -0
- package/templates/stacks/react/symbol-index.ts +81 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync, readdirSync, copyFileSync, cpSync } from "fs";
|
|
4
|
+
import { join, resolve, dirname, basename } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { createInterface } from "readline";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
|
|
12
|
+
const CLAUDE_HOME = join(
|
|
13
|
+
process.env.USERPROFILE ?? process.env.HOME ?? "~",
|
|
14
|
+
".claude"
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const TEMPLATES_DIR = resolve(__dirname, "..", "templates");
|
|
18
|
+
|
|
19
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
20
|
+
const ask = (q) => new Promise((r) => rl.question(q, r));
|
|
21
|
+
|
|
22
|
+
const command = process.argv[2];
|
|
23
|
+
|
|
24
|
+
// ─── DETECT STACK ──────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function detectStack(projectPath) {
|
|
27
|
+
// Priority order: most specific first
|
|
28
|
+
// 1. Next.js
|
|
29
|
+
for (const f of ["next.config.js", "next.config.mjs", "next.config.ts"]) {
|
|
30
|
+
if (existsSync(join(projectPath, f))) return "nextjs";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Laravel
|
|
34
|
+
if (existsSync(join(projectPath, "artisan")) && existsSync(join(projectPath, "composer.json"))) {
|
|
35
|
+
return "laravel";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 3. Check package.json deps
|
|
39
|
+
const pkgPath = join(projectPath, "package.json");
|
|
40
|
+
if (existsSync(pkgPath)) {
|
|
41
|
+
try {
|
|
42
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
43
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
44
|
+
|
|
45
|
+
// Express
|
|
46
|
+
if (allDeps["express"]) return "express";
|
|
47
|
+
|
|
48
|
+
// React (but not Next.js — already checked above)
|
|
49
|
+
if (allDeps["react"]) return "react";
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── GENERATE ──────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
async function generate() {
|
|
59
|
+
const cwd = process.cwd();
|
|
60
|
+
const projectName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
61
|
+
|
|
62
|
+
console.log("\n🔧 MCP Generator\n");
|
|
63
|
+
console.log(`📂 Project: ${cwd}`);
|
|
64
|
+
|
|
65
|
+
// 1. Detect stack
|
|
66
|
+
const detected = detectStack(cwd);
|
|
67
|
+
if (!detected) {
|
|
68
|
+
console.log("❌ Stack tidak terdeteksi. Pastikan ada:");
|
|
69
|
+
console.log(" - Laravel: composer.json + artisan");
|
|
70
|
+
console.log(" - Next.js: next.config.js/mjs/ts");
|
|
71
|
+
console.log(" - Express: package.json dengan dependency 'express'");
|
|
72
|
+
console.log(" - React: package.json dengan dependency 'react'");
|
|
73
|
+
rl.close();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log(`🔍 Stack detected: ${detected}`);
|
|
77
|
+
|
|
78
|
+
// 2. Ask project slug
|
|
79
|
+
const defaultSlug = projectName;
|
|
80
|
+
const slug = (await ask(`Project slug [${defaultSlug}]: `)).trim() || defaultSlug;
|
|
81
|
+
|
|
82
|
+
// 3. Setup output directory
|
|
83
|
+
const outputDir = join(CLAUDE_HOME, "mcp", slug);
|
|
84
|
+
if (existsSync(outputDir)) {
|
|
85
|
+
const overwrite = await ask(`⚠️ ${outputDir} sudah ada. Overwrite? (y/n): `);
|
|
86
|
+
if (overwrite.toLowerCase() !== "y") {
|
|
87
|
+
console.log("❌ Dibatalkan.");
|
|
88
|
+
rl.close();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(`\n📦 Generating MCP server for '${slug}' (${detected})...\n`);
|
|
94
|
+
|
|
95
|
+
// 4. Create directory structure
|
|
96
|
+
mkdirSync(join(outputDir, "src", "tools"), { recursive: true });
|
|
97
|
+
mkdirSync(join(outputDir, "src", "indexer"), { recursive: true });
|
|
98
|
+
|
|
99
|
+
// 5. Copy shared tools
|
|
100
|
+
const sharedDir = join(TEMPLATES_DIR, "shared");
|
|
101
|
+
const sharedFiles = ["get-claude-md.ts", "get-current-state.ts", "get-mistakes.ts", "log-task.ts", "record-mistake.ts", "update-state.ts", "predict-impact.ts"];
|
|
102
|
+
for (const f of sharedFiles) {
|
|
103
|
+
const src = join(sharedDir, f);
|
|
104
|
+
if (existsSync(src)) {
|
|
105
|
+
copyFileSync(src, join(outputDir, "src", "tools", f));
|
|
106
|
+
console.log(` ✅ shared/${f}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 6. Copy stack-specific files
|
|
111
|
+
const stackDir = join(TEMPLATES_DIR, "stacks", detected);
|
|
112
|
+
const stackConfig = JSON.parse(readFileSync(join(stackDir, "config.json"), "utf-8"));
|
|
113
|
+
|
|
114
|
+
if (existsSync(join(stackDir, "symbol-index.ts"))) {
|
|
115
|
+
copyFileSync(join(stackDir, "symbol-index.ts"), join(outputDir, "src", "indexer", "symbol-index.ts"));
|
|
116
|
+
console.log(` ✅ ${detected}/symbol-index.ts`);
|
|
117
|
+
}
|
|
118
|
+
if (existsSync(join(stackDir, "list-routes.ts"))) {
|
|
119
|
+
copyFileSync(join(stackDir, "list-routes.ts"), join(outputDir, "src", "tools", "list-routes.ts"));
|
|
120
|
+
console.log(` ✅ ${detected}/list-routes.ts`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 7. Generate index.ts
|
|
124
|
+
const indexContent = generateIndex(slug, cwd, detected, stackConfig);
|
|
125
|
+
writeFileSync(join(outputDir, "src", "index.ts"), indexContent);
|
|
126
|
+
console.log(` ✅ index.ts (generated)`);
|
|
127
|
+
|
|
128
|
+
// 8. Generate tsconfig.json
|
|
129
|
+
writeFileSync(join(outputDir, "tsconfig.json"), JSON.stringify({
|
|
130
|
+
compilerOptions: {
|
|
131
|
+
target: "ES2022", module: "Node16", moduleResolution: "Node16",
|
|
132
|
+
outDir: "./dist", rootDir: "./src", strict: true,
|
|
133
|
+
esModuleInterop: true, skipLibCheck: true, resolveJsonModule: true,
|
|
134
|
+
forceConsistentCasingInFileNames: true, declaration: false,
|
|
135
|
+
},
|
|
136
|
+
include: ["src/**/*"],
|
|
137
|
+
}, null, 2));
|
|
138
|
+
console.log(` ✅ tsconfig.json`);
|
|
139
|
+
|
|
140
|
+
// 9. Generate package.json
|
|
141
|
+
writeFileSync(join(outputDir, "package.json"), JSON.stringify({
|
|
142
|
+
name: `mcp-${slug}`,
|
|
143
|
+
version: "0.1.0",
|
|
144
|
+
description: `MCP server for ${slug} (${stackConfig.label})`,
|
|
145
|
+
type: "module",
|
|
146
|
+
main: "./dist/index.js",
|
|
147
|
+
scripts: { build: "tsc", start: "node dist/index.js" },
|
|
148
|
+
dependencies: { "@modelcontextprotocol/sdk": "^1.0.4" },
|
|
149
|
+
devDependencies: { "@types/node": "^20.11.0", typescript: "^5.4.0" },
|
|
150
|
+
}, null, 2));
|
|
151
|
+
console.log(` ✅ package.json`);
|
|
152
|
+
|
|
153
|
+
// 10. Create project-docs folder
|
|
154
|
+
const docsDir = join(CLAUDE_HOME, "project-docs", slug);
|
|
155
|
+
if (!existsSync(docsDir)) {
|
|
156
|
+
mkdirSync(docsDir, { recursive: true });
|
|
157
|
+
console.log(` ✅ Created ${docsDir}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 11. Install deps & build
|
|
161
|
+
console.log(`\n📥 Installing dependencies...`);
|
|
162
|
+
try {
|
|
163
|
+
execSync("npm install", { cwd: outputDir, stdio: "pipe" });
|
|
164
|
+
console.log(` ✅ npm install`);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
console.log(` ⚠️ npm install failed — run manually: cd ${outputDir} && npm install`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
console.log(`🔨 Building...`);
|
|
170
|
+
try {
|
|
171
|
+
execSync("npm run build", { cwd: outputDir, stdio: "pipe" });
|
|
172
|
+
console.log(` ✅ Build successful`);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.log(` ⚠️ Build failed — run manually: cd ${outputDir} && npm run build`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`\n✅ MCP server '${slug}' generated at ${outputDir}`);
|
|
178
|
+
console.log(`\nJalankan 'asap-mcp link' di project untuk register ke .mcp.json\n`);
|
|
179
|
+
|
|
180
|
+
rl.close();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function generateIndex(slug, projectPath, stack, config) {
|
|
184
|
+
const scanRoots = JSON.stringify(config.scan_roots);
|
|
185
|
+
const fileExts = JSON.stringify(config.file_extensions);
|
|
186
|
+
const hasSymbolIndex = config.symbol_index !== false;
|
|
187
|
+
const pathEscaped = projectPath.replace(/\\/g, "\\\\");
|
|
188
|
+
|
|
189
|
+
let imports = `
|
|
190
|
+
import { getClaudeMd } from "./tools/get-claude-md.js";
|
|
191
|
+
import { getCurrentState } from "./tools/get-current-state.js";
|
|
192
|
+
import { getMistakes } from "./tools/get-mistakes.js";
|
|
193
|
+
import { logTask } from "./tools/log-task.js";
|
|
194
|
+
import { updateState } from "./tools/update-state.js";
|
|
195
|
+
import { recordMistake } from "./tools/record-mistake.js";
|
|
196
|
+
import { predictImpact } from "./tools/predict-impact.js";
|
|
197
|
+
import { listRoutes } from "./tools/list-routes.js";`;
|
|
198
|
+
|
|
199
|
+
let symbolInit = "";
|
|
200
|
+
let findSymbolImport = "";
|
|
201
|
+
let symbolTools = "";
|
|
202
|
+
let symbolHandlers = "";
|
|
203
|
+
|
|
204
|
+
if (hasSymbolIndex) {
|
|
205
|
+
findSymbolImport = `
|
|
206
|
+
import { SymbolIndex } from "./indexer/symbol-index.js";`;
|
|
207
|
+
symbolInit = `const symbolIndex = new SymbolIndex(PROJECT_PATH);`;
|
|
208
|
+
|
|
209
|
+
const kindEnum = stack === "laravel"
|
|
210
|
+
? '"function", "method", "class", "any"'
|
|
211
|
+
: '"function", "component", "class", "hook", "type", "context", "any"';
|
|
212
|
+
|
|
213
|
+
symbolTools = `,
|
|
214
|
+
{
|
|
215
|
+
name: "find_symbol",
|
|
216
|
+
description: "Cari function/method/class/component di codebase. Return: name, file, line, kind.",
|
|
217
|
+
inputSchema: {
|
|
218
|
+
type: "object" as const,
|
|
219
|
+
properties: {
|
|
220
|
+
name: { type: "string", description: "Exact name (case-sensitive)" },
|
|
221
|
+
kind: { type: "string", enum: [${kindEnum}], description: "Filter by kind, default any" },
|
|
222
|
+
},
|
|
223
|
+
required: ["name"],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "get_symbol_body",
|
|
228
|
+
description: "Ambil body fungsi/method/component (default 80 baris). Pakai SETELAH find_symbol.",
|
|
229
|
+
inputSchema: {
|
|
230
|
+
type: "object" as const,
|
|
231
|
+
properties: {
|
|
232
|
+
name: { type: "string" },
|
|
233
|
+
max_lines: { type: "number", description: "Max baris, default 80" },
|
|
234
|
+
},
|
|
235
|
+
required: ["name"],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "rebuild_index",
|
|
240
|
+
description: "Re-scan semua file di project untuk update symbol index.",
|
|
241
|
+
inputSchema: { type: "object" as const, properties: {} },
|
|
242
|
+
}`;
|
|
243
|
+
|
|
244
|
+
symbolHandlers = `
|
|
245
|
+
case "find_symbol": {
|
|
246
|
+
const name = String(args?.name ?? "");
|
|
247
|
+
await symbolIndex.ensureBuilt();
|
|
248
|
+
let results = symbolIndex.find(name, args?.kind ? String(args.kind) : undefined);
|
|
249
|
+
if (results.length === 0) {
|
|
250
|
+
const rb = await symbolIndex.rebuild();
|
|
251
|
+
results = symbolIndex.find(name, args?.kind ? String(args.kind) : undefined);
|
|
252
|
+
result = { query: { name, kind: args?.kind ?? "any" }, count: results.length, results, auto_rebuilt: true, rebuild_ms: rb.ms };
|
|
253
|
+
} else {
|
|
254
|
+
result = { query: { name, kind: args?.kind ?? "any" }, count: results.length, results };
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case "get_symbol_body": {
|
|
259
|
+
const name = String(args?.name ?? "");
|
|
260
|
+
const maxLines = typeof args?.max_lines === "number" ? args.max_lines : 80;
|
|
261
|
+
await symbolIndex.ensureBuilt();
|
|
262
|
+
let results = symbolIndex.find(name);
|
|
263
|
+
if (results.length === 0) { await symbolIndex.rebuild(); results = symbolIndex.find(name); }
|
|
264
|
+
if (results.length === 0) { result = { name, found: false }; break; }
|
|
265
|
+
const first = results[0];
|
|
266
|
+
const content = await readFile(first.file, "utf-8");
|
|
267
|
+
const lines = content.split("\\n");
|
|
268
|
+
const startLine = first.line - 1;
|
|
269
|
+
const endLine = Math.min(lines.length, startLine + maxLines);
|
|
270
|
+
result = { name, file: first.file, line: first.line, kind: first.kind, body: lines.slice(startLine, endLine).join("\\n"), truncated: endLine - startLine === maxLines };
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case "rebuild_index":
|
|
274
|
+
result = await symbolIndex.rebuild();
|
|
275
|
+
break;`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return `#!/usr/bin/env node
|
|
279
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
280
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
281
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
282
|
+
import { join } from "path";
|
|
283
|
+
import { readFile } from "fs/promises";
|
|
284
|
+
${imports}
|
|
285
|
+
${findSymbolImport}
|
|
286
|
+
|
|
287
|
+
const PROJECT_SLUG = process.env.PROJECT_SLUG ?? "${slug}";
|
|
288
|
+
const PROJECT_PATH = process.env.PROJECT_PATH ?? "${pathEscaped}";
|
|
289
|
+
const CLAUDE_HOME = process.env.CLAUDE_HOME ?? \`\${process.env.USERPROFILE ?? process.env.HOME}/.claude\`;
|
|
290
|
+
const DOCS_PATH = join(CLAUDE_HOME, "project-docs", PROJECT_SLUG);
|
|
291
|
+
|
|
292
|
+
${symbolInit}
|
|
293
|
+
|
|
294
|
+
const server = new Server(
|
|
295
|
+
{ name: "${slug}", version: "0.1.0" },
|
|
296
|
+
{ capabilities: { tools: {} } }
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const tools = [
|
|
300
|
+
{ name: "get_claude_md", description: "Ambil CLAUDE.md project.", inputSchema: { type: "object" as const, properties: {} } },
|
|
301
|
+
{ name: "get_current_state", description: "Ambil CURRENT_STATE.md — task aktif + selesai.", inputSchema: { type: "object" as const, properties: {} } },
|
|
302
|
+
{ name: "get_mistakes", description: "Ambil MISTAKES.md, bisa filter by topic.", inputSchema: { type: "object" as const, properties: { topic: { type: "string", description: "Optional keyword filter" } } } },
|
|
303
|
+
{ name: "list_routes", description: "List semua routes/pages di project.", inputSchema: { type: "object" as const, properties: {} } },
|
|
304
|
+
{
|
|
305
|
+
name: "predict_impact",
|
|
306
|
+
description: "Impact analysis — cari siapa yang pakai symbol ini. Panggil sebelum refactor.",
|
|
307
|
+
inputSchema: { type: "object" as const, properties: { symbol: { type: "string", description: "Nama symbol (case-sensitive)" } }, required: ["symbol"] },
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "log_task",
|
|
311
|
+
description: "Catat task selesai ke AUDIT_LOG.md.",
|
|
312
|
+
inputSchema: { type: "object" as const, properties: { task_id: { type: "string" }, action: { type: "string" }, files: { type: "array", items: { type: "string" } }, result: { type: "string" } }, required: ["task_id", "action", "files", "result"] },
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: "update_state",
|
|
316
|
+
description: "Update CURRENT_STATE.md — tambah/complete/update task.",
|
|
317
|
+
inputSchema: { type: "object" as const, properties: { action: { type: "string", enum: ["add_task", "complete_task", "update_task"] }, task_id: { type: "string" }, title: { type: "string" }, status: { type: "string" }, scope: { type: "string" }, files_changed: { type: "array", items: { type: "string" } }, notes: { type: "string" } }, required: ["action", "task_id"] },
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: "record_mistake",
|
|
321
|
+
description: "Catat bug/gotcha ke MISTAKES.md.",
|
|
322
|
+
inputSchema: { type: "object" as const, properties: { title: { type: "string" }, found_during: { type: "string" }, file: { type: "string" }, root_cause: { type: "string" }, fix: { type: "string" }, impact: { type: "string", enum: ["low", "medium", "high"] }, related: { type: "string" } }, required: ["title", "found_during", "file", "root_cause", "fix", "impact"] },
|
|
323
|
+
}${symbolTools}
|
|
324
|
+
];
|
|
325
|
+
|
|
326
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
327
|
+
|
|
328
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
329
|
+
const { name, arguments: args } = request.params;
|
|
330
|
+
try {
|
|
331
|
+
let result: unknown;
|
|
332
|
+
switch (name) {
|
|
333
|
+
case "get_claude_md": result = await getClaudeMd(DOCS_PATH); break;
|
|
334
|
+
case "get_current_state": result = await getCurrentState(DOCS_PATH); break;
|
|
335
|
+
case "get_mistakes": result = await getMistakes(DOCS_PATH, args?.topic ? String(args.topic) : undefined); break;
|
|
336
|
+
case "list_routes": result = await listRoutes(PROJECT_PATH); break;
|
|
337
|
+
case "predict_impact": result = await predictImpact(PROJECT_PATH, ${scanRoots}, ${fileExts}, String(args?.symbol ?? "")); break;
|
|
338
|
+
case "log_task": result = await logTask(DOCS_PATH, { task_id: String(args?.task_id ?? ""), action: String(args?.action ?? ""), files: Array.isArray(args?.files) ? args.files.map(String) : [], result: String(args?.result ?? "done") }); break;
|
|
339
|
+
case "update_state": result = await updateState(DOCS_PATH, { action: String(args?.action ?? "add_task") as any, task_id: String(args?.task_id ?? ""), title: args?.title ? String(args.title) : undefined, status: args?.status ? String(args.status) : undefined, scope: args?.scope ? String(args.scope) : undefined, files_changed: Array.isArray(args?.files_changed) ? args.files_changed.map(String) : undefined, notes: args?.notes ? String(args.notes) : undefined }); break;
|
|
340
|
+
case "record_mistake": result = await recordMistake(DOCS_PATH, { title: String(args?.title ?? ""), found_during: String(args?.found_during ?? ""), file: String(args?.file ?? ""), root_cause: String(args?.root_cause ?? ""), fix: String(args?.fix ?? ""), impact: (args?.impact as any) ?? "medium", related: args?.related ? String(args.related) : undefined }); break;
|
|
341
|
+
${symbolHandlers}
|
|
342
|
+
default: throw new Error(\`Unknown tool: \${name}\`);
|
|
343
|
+
}
|
|
344
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
345
|
+
} catch (err) {
|
|
346
|
+
return { content: [{ type: "text", text: \`Error: \${err instanceof Error ? err.message : String(err)}\` }], isError: true };
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const transport = new StdioServerTransport();
|
|
351
|
+
await server.connect(transport);
|
|
352
|
+
console.error(\`[\${PROJECT_SLUG}-mcp] connected. path=\${PROJECT_PATH}\`);
|
|
353
|
+
`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ─── INIT ──────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
async function init() {
|
|
359
|
+
console.log("\n🔧 MCP Orchestrator — Setup\n");
|
|
360
|
+
|
|
361
|
+
if (!existsSync(CLAUDE_HOME)) {
|
|
362
|
+
mkdirSync(CLAUDE_HOME, { recursive: true });
|
|
363
|
+
console.log(`✅ Created ${CLAUDE_HOME}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const name = await ask("Nama developer: ");
|
|
367
|
+
const email = await ask("Email: ");
|
|
368
|
+
|
|
369
|
+
const identityPath = join(CLAUDE_HOME, "agent-identity.json");
|
|
370
|
+
if (!existsSync(identityPath)) {
|
|
371
|
+
writeFileSync(identityPath, JSON.stringify({
|
|
372
|
+
name: "Agent Fullstack",
|
|
373
|
+
specialization: "Multi-stack: Laravel, React, Next.js, Express",
|
|
374
|
+
developer: { name, email },
|
|
375
|
+
}, null, 2));
|
|
376
|
+
console.log(`✅ Created ${identityPath}`);
|
|
377
|
+
} else {
|
|
378
|
+
console.log(`⏭️ ${identityPath} already exists, skipping`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const focusPath = join(CLAUDE_HOME, "current-focus.json");
|
|
382
|
+
if (!existsSync(focusPath)) {
|
|
383
|
+
writeFileSync(focusPath, JSON.stringify({ mode: "general", active_profile: null, profiles: {} }, null, 2));
|
|
384
|
+
console.log(`✅ Created ${focusPath}`);
|
|
385
|
+
} else {
|
|
386
|
+
console.log(`⏭️ ${focusPath} already exists, skipping`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const docsPath = join(CLAUDE_HOME, "project-docs");
|
|
390
|
+
if (!existsSync(docsPath)) {
|
|
391
|
+
mkdirSync(docsPath, { recursive: true });
|
|
392
|
+
console.log(`✅ Created ${docsPath}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
console.log("\n✅ Setup selesai!\n");
|
|
396
|
+
console.log("Selanjutnya:");
|
|
397
|
+
console.log(" asap-mcp generate — Generate MCP server untuk project");
|
|
398
|
+
console.log(" asap-mcp link — Link semua MCP ke .mcp.json\n");
|
|
399
|
+
|
|
400
|
+
rl.close();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ─── LINK ──────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
function link() {
|
|
406
|
+
const cwd = process.cwd();
|
|
407
|
+
const mcpConfigPath = join(cwd, ".mcp.json");
|
|
408
|
+
|
|
409
|
+
let config = { mcpServers: {} };
|
|
410
|
+
if (existsSync(mcpConfigPath)) {
|
|
411
|
+
try {
|
|
412
|
+
config = JSON.parse(readFileSync(mcpConfigPath, "utf-8"));
|
|
413
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
414
|
+
} catch { config = { mcpServers: {} }; }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 1. Orchestrator from npm
|
|
418
|
+
const orchestratorPath = resolve(__dirname, "..", "dist", "index.js");
|
|
419
|
+
if (existsSync(orchestratorPath)) {
|
|
420
|
+
const action = config.mcpServers.orchestrator ? "🔄 Updated" : "✅ Added";
|
|
421
|
+
config.mcpServers.orchestrator = {
|
|
422
|
+
command: "node",
|
|
423
|
+
args: [orchestratorPath.replace(/\\/g, "/")],
|
|
424
|
+
env: { CLAUDE_HOME: CLAUDE_HOME.replace(/\\/g, "/") },
|
|
425
|
+
};
|
|
426
|
+
console.log(`${action} 'orchestrator' (from npm package)`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 2. Scan ~/.claude/mcp/ for all MCP servers
|
|
430
|
+
const mcpRoot = join(CLAUDE_HOME, "mcp");
|
|
431
|
+
if (existsSync(mcpRoot)) {
|
|
432
|
+
try {
|
|
433
|
+
const entries = readdirSync(mcpRoot, { withFileTypes: true });
|
|
434
|
+
for (const entry of entries) {
|
|
435
|
+
if (!entry.isDirectory()) continue;
|
|
436
|
+
if (entry.name === "orchestrator") continue;
|
|
437
|
+
const serverIndex = join(mcpRoot, entry.name, "dist", "index.js");
|
|
438
|
+
if (!existsSync(serverIndex)) continue;
|
|
439
|
+
|
|
440
|
+
const serverName = entry.name;
|
|
441
|
+
const action = config.mcpServers[serverName] ? "🔄 Updated" : "✅ Added";
|
|
442
|
+
config.mcpServers[serverName] = {
|
|
443
|
+
command: "node",
|
|
444
|
+
args: [serverIndex.replace(/\\/g, "/")],
|
|
445
|
+
env: { CLAUDE_HOME: CLAUDE_HOME.replace(/\\/g, "/") },
|
|
446
|
+
};
|
|
447
|
+
console.log(`${action} '${serverName}' (from ~/.claude/mcp/${entry.name}/)`);
|
|
448
|
+
}
|
|
449
|
+
} catch {}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const serverCount = Object.keys(config.mcpServers).length;
|
|
453
|
+
writeFileSync(mcpConfigPath, JSON.stringify(config, null, 2) + "\n");
|
|
454
|
+
console.log(`\n📄 ${mcpConfigPath}`);
|
|
455
|
+
console.log(`🔗 ${serverCount} MCP server(s) linked — mereka satu tim sekarang.`);
|
|
456
|
+
console.log("Restart Claude Code jika sudah running.\n");
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ─── INFO ──────────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
function info() {
|
|
462
|
+
console.log("\n📦 @agilsee/mcp-orchestrator");
|
|
463
|
+
console.log(` CLAUDE_HOME: ${CLAUDE_HOME}`);
|
|
464
|
+
console.log("\nCommands:");
|
|
465
|
+
console.log(" asap-mcp init — Setup ~/.claude (identity, focus, project-docs)");
|
|
466
|
+
console.log(" asap-mcp generate — Auto-detect stack & generate MCP server untuk project");
|
|
467
|
+
console.log(" asap-mcp link — Link semua MCP servers ke .mcp.json");
|
|
468
|
+
console.log(" asap-mcp dashboard — Open Memory Dashboard (localhost:37800)");
|
|
469
|
+
console.log(" asap-mcp info — Show this info");
|
|
470
|
+
console.log("\nSupported stacks: Laravel, Next.js, Express, React (Vite/CRA)");
|
|
471
|
+
console.log("Memory Dashboard: http://localhost:37800\n");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ─── MAIN ──────────────────────────────────────────────────────
|
|
475
|
+
|
|
476
|
+
switch (command) {
|
|
477
|
+
case "init": await init(); break;
|
|
478
|
+
case "generate": await generate(); break;
|
|
479
|
+
case "link": link(); break;
|
|
480
|
+
case "info": info(); break;
|
|
481
|
+
case "dashboard": case "dash": case "ui": {
|
|
482
|
+
const { execSync, spawn } = await import("child_process");
|
|
483
|
+
const serverPath = path.join(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "..", "dist", "server", "web-server.js");
|
|
484
|
+
console.log("Starting MCP Memory Dashboard...");
|
|
485
|
+
const child = spawn("node", [serverPath], { stdio: "inherit", env: { ...process.env, CLAUDE_HOME: CLAUDE_HOME } });
|
|
486
|
+
child.on("error", (err) => { console.error("Failed to start dashboard:", err.message); });
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
default: info(); break;
|
|
490
|
+
}
|