@agilemonster/mcp-server 1.0.15 → 1.0.16

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/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AgileMonsterProjectConfig, AutonomousStepResponse, Board, BoardAgentConfig, Card, ContextResponse, DiscoveredProject, PulseResponse } from './types.js';
2
- export declare const MCP_SERVER_VERSION = "1.0.15";
2
+ export declare const MCP_SERVER_VERSION = "1.0.16";
3
3
  export declare class AgileMonsterClient {
4
4
  private apiKey;
5
5
  private baseUrl;
@@ -50,6 +50,15 @@ export declare class AgileMonsterClient {
50
50
  default_model?: string | null;
51
51
  models: Record<string, string>;
52
52
  }>;
53
+ /**
54
+ * Auto-detect build command, deployment method, deploy script, and live URL for a project repository.
55
+ */
56
+ static inspectProjectProfile(fullPath: string, name: string): {
57
+ build_command: string | null;
58
+ deploy_method: string | null;
59
+ deploy_command: string | null;
60
+ live_url: string | null;
61
+ };
53
62
  /**
54
63
  * Scan the developer's local workspace roots (sibling folders in parent dev dir, ~/dev, ~/Projects, etc.)
55
64
  * to discover real project folders so the web UI can auto-match and let the user pick from a dropdown.
@@ -84,5 +93,6 @@ export declare class AgileMonsterClient {
84
93
  action: 'select_files' | 'generate_edits';
85
94
  file_index?: string[];
86
95
  files?: Record<string, string>;
96
+ project_context?: string;
87
97
  }): Promise<AutonomousStepResponse>;
88
98
  }
package/dist/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execSync } from 'node:child_process';
4
- export const MCP_SERVER_VERSION = '1.0.15';
4
+ export const MCP_SERVER_VERSION = '1.0.16';
5
5
  export class AgileMonsterClient {
6
6
  apiKey = '';
7
7
  baseUrl;
@@ -315,17 +315,117 @@ export class AgileMonsterClient {
315
315
  catch { }
316
316
  // 5. Antigravity
317
317
  result.antigravity = {
318
- default_model: 'gemini-3.1-pro',
318
+ default_model: 'gemini-4-argon',
319
319
  models: {
320
+ 'gemini-4-argon': 'Gemini 4 Argon (Fallback -> Claude Opus 5.5)',
321
+ 'claude-opus-5-5': 'Claude Opus 5.5',
320
322
  'gemini-3.1-pro': 'Gemini 3.1 Pro',
321
323
  'gemini-3.0-flash': 'Gemini 3.0 Flash',
322
- 'claude-opus-5-5': 'Claude Opus 5.5',
323
324
  'claude-sonnet-5-5': 'Claude Sonnet 5.5',
324
325
  'gpt-6.1-sol': 'GPT-6.1 Sol',
325
326
  },
326
327
  };
327
328
  return result;
328
329
  }
330
+ /**
331
+ * Auto-detect build command, deployment method, deploy script, and live URL for a project repository.
332
+ */
333
+ static inspectProjectProfile(fullPath, name) {
334
+ let buildCmd = null;
335
+ let deployMethod = null;
336
+ let deployCmd = null;
337
+ let liveUrl = null;
338
+ // 1. Detect Build Command
339
+ try {
340
+ const fePkg = path.join(fullPath, 'frontend', 'package.json');
341
+ const rootPkg = path.join(fullPath, 'package.json');
342
+ if (fs.existsSync(fePkg)) {
343
+ const pkg = JSON.parse(fs.readFileSync(fePkg, 'utf8'));
344
+ if (pkg?.scripts?.build) {
345
+ buildCmd = fs.existsSync(path.join(fullPath, 'frontend', 'pnpm-lock.yaml'))
346
+ ? 'cd frontend && pnpm run build'
347
+ : 'cd frontend && npm run build';
348
+ }
349
+ }
350
+ else if (fs.existsSync(rootPkg)) {
351
+ const pkg = JSON.parse(fs.readFileSync(rootPkg, 'utf8'));
352
+ if (pkg?.scripts?.build) {
353
+ buildCmd = fs.existsSync(path.join(fullPath, 'pnpm-lock.yaml'))
354
+ ? 'pnpm run build'
355
+ : 'npm run build';
356
+ }
357
+ }
358
+ else if (fs.existsSync(path.join(fullPath, 'frontend-flutter', 'pubspec.yaml'))) {
359
+ buildCmd = 'cd frontend-flutter && flutter analyze';
360
+ }
361
+ else if (fs.existsSync(path.join(fullPath, 'pubspec.yaml'))) {
362
+ buildCmd = 'flutter analyze';
363
+ }
364
+ }
365
+ catch { }
366
+ // 2. Detect Deploy Method & Command
367
+ try {
368
+ const deploySh = path.join(fullPath, 'deploy', 'deploy.sh');
369
+ const scriptsDeploySh = path.join(fullPath, 'scripts', 'deploy.sh');
370
+ if (fs.existsSync(deploySh)) {
371
+ deployMethod = 'deploy_script';
372
+ const content = fs.readFileSync(deploySh, 'utf8');
373
+ deployCmd = content.includes('--skip-preflight')
374
+ ? './deploy/deploy.sh --skip-preflight'
375
+ : './deploy/deploy.sh';
376
+ }
377
+ else if (fs.existsSync(scriptsDeploySh)) {
378
+ deployMethod = 'deploy_script';
379
+ deployCmd = './scripts/deploy.sh';
380
+ }
381
+ else if (fs.existsSync(path.join(fullPath, '.github', 'workflows'))) {
382
+ deployMethod = 'github_actions';
383
+ deployCmd = 'git push origin HEAD (GitHub Actions CI/CD)';
384
+ }
385
+ else if (fs.existsSync(path.join(fullPath, 'docker-compose.yml'))) {
386
+ deployMethod = 'docker_compose';
387
+ deployCmd = 'docker compose up -d --build';
388
+ }
389
+ else if (fs.existsSync(path.join(fullPath, '.git'))) {
390
+ deployMethod = 'git_push';
391
+ deployCmd = 'git push origin HEAD';
392
+ }
393
+ }
394
+ catch { }
395
+ // 3. Detect Live URL
396
+ try {
397
+ for (const docFile of ['CLAUDE.md', 'AGENTS.md', 'README.md', '.env']) {
398
+ const docPath = path.join(fullPath, docFile);
399
+ if (fs.existsSync(docPath)) {
400
+ const text = fs.readFileSync(docPath, 'utf8').slice(0, 6000);
401
+ const urlMatch = text.match(/https:\/\/(?:www\.)?([a-z0-9-]+\.(?:com|ai|io|app|dev|org|net))\b/i);
402
+ if (urlMatch && !/github\.com|openai\.com|anthropic\.com|googleapis\.com|npmjs\.com|localhost/i.test(urlMatch[0])) {
403
+ liveUrl = urlMatch[0];
404
+ break;
405
+ }
406
+ }
407
+ }
408
+ if (!liveUrl) {
409
+ const clean = name.toLowerCase().replace(/^nieuw\.games[-_]*/i, '');
410
+ if (/^[a-z0-9-]+\.(com|ai|io|app|dev|org|net)$/i.test(clean)) {
411
+ liveUrl = `https://${clean}`;
412
+ }
413
+ else if (clean === 'meerlink') {
414
+ liveUrl = 'https://meerlink.com';
415
+ }
416
+ else if (clean === 'werkinghard' || clean === 'workinghard') {
417
+ liveUrl = 'https://werkinghard.com';
418
+ }
419
+ }
420
+ }
421
+ catch { }
422
+ return {
423
+ build_command: buildCmd,
424
+ deploy_method: deployMethod,
425
+ deploy_command: deployCmd,
426
+ live_url: liveUrl,
427
+ };
428
+ }
329
429
  /**
330
430
  * Scan the developer's local workspace roots (sibling folders in parent dev dir, ~/dev, ~/Projects, etc.)
331
431
  * to discover real project folders so the web UI can auto-match and let the user pick from a dropdown.
@@ -405,10 +505,12 @@ export class AgileMonsterClient {
405
505
  }
406
506
  catch { }
407
507
  }
508
+ const profile = AgileMonsterClient.inspectProjectProfile(fullPath, name);
408
509
  found.set(fullPath, {
409
510
  name,
410
511
  path: fullPath,
411
512
  git_remote: gitRemote,
513
+ ...profile,
412
514
  });
413
515
  }
414
516
  }
package/dist/server.d.ts CHANGED
@@ -36,7 +36,13 @@ export declare function createServer(client?: AgileMonsterClient): Server<{
36
36
  } | undefined;
37
37
  }>;
38
38
  export declare function executeQueuedCardAutonomously(client: AgileMonsterClient, card: Card, projectPath: string): Promise<boolean>;
39
+ /**
40
+ * Deploy a card the human approved into Done (or flagged as `deploying`).
41
+ * Idempotent: keyed on card ID only (never on `updated_at`, which changes whenever we post a comment),
42
+ * so a card is deployed at most once unless the human explicitly re-triggers it into `deploying`.
43
+ */
39
44
  export declare function deployApprovedCardIfNeeded(client: AgileMonsterClient, card: Card, projectPath: string): Promise<boolean>;
40
- export declare function startBuiltInPullPulse(server: Server | null, client: AgileMonsterClient, intervalMs?: number): NodeJS.Timeout;
45
+ export declare function acquireWorkerLock(role: 'daemon' | 'stdio'): boolean;
46
+ export declare function startBuiltInPullPulse(server: Server | null, client: AgileMonsterClient, intervalMs?: number, role?: 'daemon' | 'stdio'): NodeJS.Timeout;
41
47
  export declare function runStdioServer(): Promise<void>;
42
48
  export declare function runBackgroundPulseDaemon(intervalMs?: number): Promise<void>;
package/dist/server.js CHANGED
@@ -761,15 +761,9 @@ function syncIdeTaskRules(boardName, queuedCards) {
761
761
  }
762
762
  catch { }
763
763
  }
764
- function triggerBackgroundSelfUpdate() {
764
+ function triggerBackgroundSelfUpdate(targetVersion) {
765
765
  try {
766
- // 1. Pre-warm latest @agilemonster/mcp-server in npx cache
767
- const child = spawn('npx', ['-y', '@agilemonster/mcp-server@latest', '--version'], {
768
- stdio: 'ignore',
769
- detached: true,
770
- });
771
- child.unref();
772
- // 2. Ensure local .cursor/mcp.json and .mcp.json use @latest so next IDE reload uses latest
766
+ // 1. Ensure local .cursor/mcp.json and .mcp.json use @latest so next IDE reload uses latest
773
767
  const cwd = process.cwd();
774
768
  for (const rel of ['.cursor/mcp.json', '.mcp.json', '.vscode/mcp.json']) {
775
769
  const p = path.join(cwd, rel);
@@ -784,8 +778,56 @@ function triggerBackgroundSelfUpdate() {
784
778
  catch { }
785
779
  }
786
780
  }
781
+ // 2. Install @agilemonster/mcp-server@latest into global/npx cache and then exit(0) so launchd respawns the daemon cleanly
782
+ const child = spawn('sh', ['-c', 'npm install -g @agilemonster/mcp-server@latest >/dev/null 2>&1 || npx -y @agilemonster/mcp-server@latest --version >/dev/null 2>&1'], {
783
+ stdio: 'ignore',
784
+ detached: true,
785
+ });
786
+ child.on('exit', () => {
787
+ appendIdeActivityLog(cwd, {
788
+ cardId: 'system-update',
789
+ cardTitle: 'MCP Daemon Auto-Update',
790
+ agentName: 'Agile Monster Daemon',
791
+ model: 'system',
792
+ status: 'updated',
793
+ summary: `Automatically updated @agilemonster/mcp-server from v${MCP_SERVER_VERSION} to v${targetVersion || 'latest'} and restarted daemon via launchd.`,
794
+ });
795
+ setTimeout(() => {
796
+ process.exit(0);
797
+ }, 1500).unref();
798
+ });
799
+ child.unref();
800
+ }
801
+ catch { }
802
+ }
803
+ function readProjectInstructionsContext(projectPath) {
804
+ const sections = [];
805
+ for (const docName of ['CLAUDE.md', 'AGENTS.md', 'README.md']) {
806
+ const docPath = path.join(projectPath, docName);
807
+ if (fs.existsSync(docPath)) {
808
+ try {
809
+ const raw = fs.readFileSync(docPath, 'utf8').trim();
810
+ if (raw.length > 0) {
811
+ sections.push(`=== ${docName} ===\n${raw.slice(0, 1800)}`);
812
+ }
813
+ }
814
+ catch { }
815
+ }
816
+ if (sections.join('\n\n').length >= 3000)
817
+ break;
818
+ }
819
+ try {
820
+ const recentCommits = execSync('git log -n 4 --oneline', {
821
+ cwd: projectPath,
822
+ encoding: 'utf8',
823
+ stdio: ['ignore', 'pipe', 'ignore'],
824
+ }).trim();
825
+ if (recentCommits) {
826
+ sections.push(`=== Recent Git Commits ===\n${recentCommits}`);
827
+ }
787
828
  }
788
829
  catch { }
830
+ return sections.join('\n\n').slice(0, 3800);
789
831
  }
790
832
  const activeAutonomousCards = new Set();
791
833
  function collectProjectSourceFiles(projectPath, keywords) {
@@ -989,10 +1031,12 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
989
1031
  return false;
990
1032
  }
991
1033
  // 3. Ask Agile Monster Autonomous Step to pick the 1-6 most relevant files
1034
+ const projectContext = readProjectInstructionsContext(projectPath);
992
1035
  const selectionRes = await client.autonomousStep({
993
1036
  card_id: card.id,
994
1037
  action: 'select_files',
995
1038
  file_index: fileIndex,
1039
+ project_context: projectContext,
996
1040
  });
997
1041
  const selectedFiles = (selectionRes.target_files || selectionRes.selected_files || []).slice(0, 6);
998
1042
  if (selectedFiles.length === 0) {
@@ -1033,7 +1077,27 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
1033
1077
  card_id: card.id,
1034
1078
  action: 'generate_edits',
1035
1079
  files: filesPayload,
1080
+ project_context: projectContext,
1036
1081
  });
1082
+ // Gate A: Strict truth — if the model is not confident / cannot find the target, ask the human instead of guessing
1083
+ if (editsRes.needs_clarification && (editsRes.clarification_question || '').trim() !== '') {
1084
+ try {
1085
+ if (fs.existsSync(lockFile))
1086
+ fs.unlinkSync(lockFile);
1087
+ }
1088
+ catch { }
1089
+ appendIdeActivityLog(projectPath, {
1090
+ cardId: card.id,
1091
+ cardTitle: card.title,
1092
+ agentName: workerLabel,
1093
+ model: card.agent_model || 'auto',
1094
+ status: 'waiting_for_input',
1095
+ summary: `Needs clarification: ${editsRes.clarification_question}`,
1096
+ files: Object.keys(filesPayload),
1097
+ });
1098
+ await client.askQuestion(card.id, provider, (editsRes.clarification_question || '').trim());
1099
+ return false;
1100
+ }
1037
1101
  const applyEditsPass = (editsList) => {
1038
1102
  const modFiles = [];
1039
1103
  for (const edit of editsList) {
@@ -1153,6 +1217,7 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
1153
1217
  card_id: card.id,
1154
1218
  action: 'generate_edits',
1155
1219
  files: retryFiles,
1220
+ project_context: projectContext,
1156
1221
  });
1157
1222
  const retryMods = applyEditsPass(editsRes.edits || []);
1158
1223
  modifiedFiles.push(...retryMods);
@@ -1178,9 +1243,33 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
1178
1243
  }).trim();
1179
1244
  }
1180
1245
  catch { }
1181
- // Automatically commit, push, and run the project's defined deployment pipeline (e.g. deploy/deploy.sh, GitHub Actions, or Docker)
1182
- // so changes are live for testing as soon as the card moves to Testing / QA!
1183
- const deployInfo = runProjectDeploymentPipeline(projectPath, card.title || 'update', card.id.slice(0, 8));
1246
+ // Gate C: Commit, push, and run the project's defined deployment pipeline, then VERIFY it actually succeeded.
1247
+ // If deploy_stage is 'done' or 'manual', only commit locally-verified changes and defer deployment to approval.
1248
+ const deployStage = (card.deploy_stage || 'review').toLowerCase();
1249
+ const deployInfo = deployStage === 'review'
1250
+ ? await runProjectDeploymentPipeline(projectPath, card.title || 'update', card.id.slice(0, 8), false, card)
1251
+ : { ok: true, commitHash: '', branch: '', deployedNote: '', errorDetail: '' };
1252
+ if (!deployInfo.ok) {
1253
+ // Never claim success: keep card In Progress (waiting_for_input) with a plain-English diagnostic
1254
+ try {
1255
+ if (fs.existsSync(lockFile))
1256
+ fs.unlinkSync(lockFile);
1257
+ }
1258
+ catch { }
1259
+ const reason = (deployInfo.errorDetail || 'Unknown deployment error').slice(0, 700);
1260
+ appendIdeActivityLog(projectPath, {
1261
+ cardId: card.id,
1262
+ cardTitle: card.title,
1263
+ agentName: workerLabel,
1264
+ model: card.agent_model || 'auto',
1265
+ status: 'deploy_failed',
1266
+ summary: `Code change was made but deployment could not be verified: ${reason}`,
1267
+ files: modifiedFiles,
1268
+ diff: rawDiff,
1269
+ });
1270
+ await client.askQuestion(card.id, provider, `I made the code change (${modifiedFiles.join(', ')}), but the deployment could not be verified, so I kept this card in progress instead of moving it to Testing.\n\nWhat failed: ${reason}\n\nReply "retry" once the issue is fixed, or tell me how this project should be deployed.`);
1271
+ return false;
1272
+ }
1184
1273
  const baseSummary = editsRes.summary ||
1185
1274
  `Updated ${modifiedFiles.join(', ')} to complete "${card.title}".`;
1186
1275
  const cleanPlainSummary = baseSummary.replace(/```[\s\S]*?```/g, '').trim() +
@@ -1228,9 +1317,12 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
1228
1317
  fs.unlinkSync(lockFile);
1229
1318
  }
1230
1319
  catch { }
1231
- await client.askQuestion(card.id, provider, `Inspected ${Object.keys(filesPayload)
1320
+ const buildNote = buildCheck && !buildCheck.ok && buildCheck.errorOutput
1321
+ ? `\n\nThe attempted change did not compile, so it was reverted. Last build error:\n${buildCheck.errorOutput.split('\n').filter((l) => /error/i.test(l)).slice(0, 4).join('\n').slice(0, 600)}`
1322
+ : '';
1323
+ await client.askQuestion(card.id, provider, `I inspected ${Object.keys(filesPayload)
1232
1324
  .map((f) => `\`${f}\``)
1233
- .join(', ')}, but needed more specific details to apply a verified code change automatically. Reply in the comments with additional details (or open your IDE agent) to continue!`);
1325
+ .join(', ')} but could not make a verified change automatically.${buildNote}\n\nCould you describe exactly where in the app this should appear (page or screen name) and what you expect to see?`);
1234
1326
  return false;
1235
1327
  }
1236
1328
  }
@@ -1251,130 +1343,270 @@ export async function executeQueuedCardAutonomously(client, card, projectPath) {
1251
1343
  }
1252
1344
  }
1253
1345
  /**
1254
- * Detect and execute the project's defined deployment pipeline:
1255
- * 1. Commits any modified tracked files and pushes to `origin/<branch>`.
1256
- * 2. Runs `deploy/deploy.sh` (or `scripts/deploy.sh`) if present in the repo (e.g. Meerlink cPanel SSH deploy).
1257
- * 3. Or triggers GitHub Actions CI/CD via `git push`.
1346
+ * Detect and execute the project's defined deployment pipeline, then VERIFY the result:
1347
+ * 1. Commits modified tracked files and pushes to `origin/<branch>`.
1348
+ * 2. Runs the configured/auto-detected deploy method:
1349
+ * - `deploy_script` (e.g. `./deploy/deploy.sh --skip-preflight`): requires exit code 0.
1350
+ * - `github_actions`: polls `gh run list --commit <sha>` until the workflow concludes; a failed run = not deployed.
1351
+ * - `docker_compose`: runs the deploy command and checks `docker compose ps` for running containers.
1352
+ * 3. If a `live_url` is known, performs an HTTP smoke check (must return < 400).
1353
+ * Returns ok=false with a plain-English `errorDetail` whenever deployment cannot be proven.
1258
1354
  */
1259
- function runProjectDeploymentPipeline(projectPath, rawTitle, shortId, forceEmptyCommit = false) {
1260
- const cleanTitle = (rawTitle || 'update').replace(/["`\r\n]+/g, ' ').trim();
1355
+ async function runProjectDeploymentPipeline(projectPath, rawTitle, shortId, forceEmptyCommit = false, profile) {
1356
+ const cleanTitle = (rawTitle || 'update').replace(/["`$\\\r\n]+/g, ' ').trim();
1261
1357
  let branch = 'main';
1262
1358
  let commitHash = '';
1359
+ let fullSha = '';
1263
1360
  const actions = [];
1361
+ const run = (cmd, timeout = 45000) => execSync(cmd, { cwd: projectPath, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout }).trim();
1362
+ const errText = (e) => (String(e?.stderr || '') + '\n' + String(e?.stdout || '') + '\n' + String(e?.message || ''))
1363
+ .split('\n')
1364
+ .map((l) => l.trim())
1365
+ .filter((l) => l.length > 0)
1366
+ .slice(-6)
1367
+ .join(' | ')
1368
+ .slice(0, 700);
1369
+ const detected = AgileMonsterClient.inspectProjectProfile(projectPath, path.basename(projectPath));
1370
+ const deployMethod = (profile?.deploy_method || detected.deploy_method || 'git_push').toLowerCase();
1371
+ const deployCommand = profile?.deploy_command || detected.deploy_command || '';
1372
+ const liveUrl = profile?.live_url || detected.live_url || '';
1373
+ // Step 1: commit & push
1264
1374
  try {
1265
- branch = execSync('git rev-parse --abbrev-ref HEAD', {
1266
- cwd: projectPath,
1267
- encoding: 'utf8',
1268
- stdio: ['ignore', 'pipe', 'ignore'],
1269
- }).trim() || 'main';
1270
- const statusOut = execSync('git status --porcelain -uno', {
1271
- cwd: projectPath,
1272
- encoding: 'utf8',
1273
- stdio: ['ignore', 'pipe', 'ignore'],
1274
- }).trim();
1375
+ branch = run('git rev-parse --abbrev-ref HEAD') || 'main';
1376
+ const statusOut = run('git status --porcelain -uno');
1275
1377
  if (statusOut.length > 0) {
1276
- execSync('git add -u', { cwd: projectPath, stdio: 'ignore' });
1277
- execSync(`git commit -m "feat: ${cleanTitle} [AM-${shortId}]"`, {
1278
- cwd: projectPath,
1279
- stdio: 'ignore',
1280
- });
1281
- execSync(`git push origin ${branch}`, {
1282
- cwd: projectPath,
1283
- stdio: 'ignore',
1284
- timeout: 45000,
1285
- });
1286
- commitHash = execSync('git rev-parse --short HEAD', {
1287
- cwd: projectPath,
1288
- encoding: 'utf8',
1289
- stdio: ['ignore', 'pipe', 'ignore'],
1290
- }).trim();
1291
- actions.push(`pushed \`${commitHash}\` to \`origin/${branch}\``);
1378
+ run('git add -u');
1379
+ run(`git commit -m "feat: ${cleanTitle} [AM-${shortId}]"`);
1292
1380
  }
1293
1381
  else if (forceEmptyCommit) {
1294
- execSync(`git commit --allow-empty -m "chore: trigger container redeploy [AM-${shortId}]"`, {
1295
- cwd: projectPath,
1296
- stdio: 'ignore',
1297
- });
1298
- execSync(`git push origin ${branch}`, {
1299
- cwd: projectPath,
1300
- stdio: 'ignore',
1301
- timeout: 45000,
1302
- });
1303
- commitHash = execSync('git rev-parse --short HEAD', {
1304
- cwd: projectPath,
1305
- encoding: 'utf8',
1306
- stdio: ['ignore', 'pipe', 'ignore'],
1307
- }).trim();
1308
- actions.push(`triggered redeploy commit \`${commitHash}\` on \`origin/${branch}\``);
1382
+ run(`git commit --allow-empty -m "chore: trigger redeploy [AM-${shortId}]"`);
1309
1383
  }
1310
- else {
1311
- commitHash = execSync('git rev-parse --short HEAD', {
1312
- cwd: projectPath,
1313
- encoding: 'utf8',
1314
- stdio: ['ignore', 'pipe', 'ignore'],
1315
- }).trim();
1384
+ fullSha = run('git rev-parse HEAD');
1385
+ commitHash = fullSha.slice(0, 7);
1386
+ if (fs.existsSync(path.join(projectPath, '.git')) && run('git remote').length > 0) {
1387
+ try {
1388
+ run(`git push origin ${branch}`, 60000);
1389
+ actions.push(`pushed \`${commitHash}\` to \`origin/${branch}\``);
1390
+ }
1391
+ catch (pushErr) {
1392
+ return {
1393
+ ok: false,
1394
+ commitHash,
1395
+ branch,
1396
+ deployedNote: '',
1397
+ errorDetail: `git push to origin/${branch} failed (check credentials or branch protection): ${errText(pushErr)}`,
1398
+ };
1399
+ }
1316
1400
  }
1317
- // Check if the project has a dedicated deploy script (e.g. deploy/deploy.sh in Meerlink)
1318
- const deployScript = path.join(projectPath, 'deploy', 'deploy.sh');
1319
- if (fs.existsSync(deployScript)) {
1320
- const scriptContent = fs.readFileSync(deployScript, 'utf8');
1321
- const flag = scriptContent.includes('--skip-preflight') ? ' --skip-preflight' : '';
1322
- execSync(`bash "${deployScript}"${flag}`, {
1323
- cwd: projectPath,
1324
- stdio: ['ignore', 'pipe', 'pipe'],
1325
- timeout: 180000,
1326
- });
1327
- actions.push('deployed live via `deploy/deploy.sh`');
1401
+ }
1402
+ catch (gitErr) {
1403
+ return { ok: false, commitHash, branch, deployedNote: '', errorDetail: `Git commit failed: ${errText(gitErr)}` };
1404
+ }
1405
+ // Step 2: run & verify the deploy method
1406
+ try {
1407
+ if (deployMethod === 'deploy_script') {
1408
+ const cmd = deployCommand && !deployCommand.includes('(')
1409
+ ? deployCommand
1410
+ : './deploy/deploy.sh';
1411
+ const scriptPath = cmd.split(' ')[0];
1412
+ const args = cmd.split(' ').slice(1).join(' ');
1413
+ try {
1414
+ run(`bash "${path.resolve(projectPath, scriptPath)}" ${args}`, 300000);
1415
+ }
1416
+ catch (dErr) {
1417
+ return {
1418
+ ok: false,
1419
+ commitHash,
1420
+ branch,
1421
+ deployedNote: '',
1422
+ errorDetail: `Deploy script \`${cmd}\` exited with an error: ${errText(dErr)}`,
1423
+ };
1424
+ }
1425
+ actions.push(`deployed via \`${cmd}\` (exit 0)`);
1328
1426
  }
1329
- else if (fs.existsSync(path.join(projectPath, '.github', 'workflows'))) {
1330
- actions.push('triggered GitHub Actions CI/CD deployment');
1427
+ else if (deployMethod === 'github_actions') {
1428
+ let hasGh = false;
1429
+ try {
1430
+ run('gh --version', 5000);
1431
+ hasGh = true;
1432
+ }
1433
+ catch { }
1434
+ if (!hasGh) {
1435
+ actions.push('triggered GitHub Actions (could not verify: `gh` CLI not installed)');
1436
+ }
1437
+ else {
1438
+ const deadline = Date.now() + 8 * 60 * 1000;
1439
+ let verdict = 'none';
1440
+ let failedName = '';
1441
+ let runUrl = '';
1442
+ // Give GitHub a moment to register the workflow run
1443
+ await new Promise((r) => setTimeout(r, 8000));
1444
+ while (Date.now() < deadline) {
1445
+ let runs = [];
1446
+ try {
1447
+ runs = JSON.parse(run(`gh run list --commit ${fullSha} --json status,conclusion,name,url --limit 10`, 20000) || '[]');
1448
+ }
1449
+ catch { }
1450
+ if (runs.length === 0) {
1451
+ verdict = 'none';
1452
+ }
1453
+ else if (runs.some((r) => r.status !== 'completed')) {
1454
+ verdict = 'pending';
1455
+ }
1456
+ else {
1457
+ const failed = runs.find((r) => r.conclusion !== 'success' && r.conclusion !== 'skipped');
1458
+ verdict = failed ? 'failure' : 'success';
1459
+ failedName = failed?.name || '';
1460
+ runUrl = (failed || runs[0])?.url || '';
1461
+ break;
1462
+ }
1463
+ if (verdict === 'none' && Date.now() > deadline - 7 * 60 * 1000)
1464
+ break; // no workflow for this commit after ~1 min
1465
+ await new Promise((r) => setTimeout(r, 15000));
1466
+ }
1467
+ if (verdict === 'failure') {
1468
+ return {
1469
+ ok: false,
1470
+ commitHash,
1471
+ branch,
1472
+ deployedNote: '',
1473
+ errorDetail: `GitHub Actions workflow "${failedName}" failed for commit ${commitHash}. ${runUrl}`.trim(),
1474
+ };
1475
+ }
1476
+ if (verdict === 'pending') {
1477
+ return {
1478
+ ok: false,
1479
+ commitHash,
1480
+ branch,
1481
+ deployedNote: '',
1482
+ errorDetail: `GitHub Actions for commit ${commitHash} was still running after 8 minutes. ${runUrl}`.trim(),
1483
+ };
1484
+ }
1485
+ actions.push(verdict === 'success'
1486
+ ? `GitHub Actions passed for \`${commitHash}\``
1487
+ : 'pushed (no GitHub Actions workflow ran for this commit)');
1488
+ }
1489
+ }
1490
+ else if (deployMethod === 'docker_compose') {
1491
+ const cmd = deployCommand || 'docker compose up -d --build';
1492
+ try {
1493
+ run(cmd, 600000);
1494
+ const ps = run('docker compose ps --format json', 20000);
1495
+ if (/"State"\s*:\s*"(exited|dead|restarting)"/i.test(ps)) {
1496
+ return { ok: false, commitHash, branch, deployedNote: '', errorDetail: 'A Docker container is not running after deploy (exited/restarting).' };
1497
+ }
1498
+ }
1499
+ catch (dErr) {
1500
+ return { ok: false, commitHash, branch, deployedNote: '', errorDetail: `Docker deploy failed: ${errText(dErr)}` };
1501
+ }
1502
+ actions.push('Docker containers rebuilt and running');
1331
1503
  }
1332
- const deployedNote = actions.length > 0
1333
- ? `🚀 **Deployed**: Automatically ${actions.join(' and ')}.`
1334
- : `🚀 **Deployed**: Verified commit \`${commitHash}\` is live on \`origin/${branch}\`.`;
1335
- return { ok: true, commitHash, branch, deployedNote };
1336
1504
  }
1337
- catch (err) {
1338
- return {
1339
- ok: false,
1340
- commitHash,
1341
- branch,
1342
- deployedNote: '',
1343
- };
1505
+ catch (anyErr) {
1506
+ return { ok: false, commitHash, branch, deployedNote: '', errorDetail: errText(anyErr) };
1344
1507
  }
1508
+ // Step 3: live URL smoke check
1509
+ if (liveUrl && /^https?:\/\//i.test(liveUrl)) {
1510
+ let status = 0;
1511
+ for (let attempt = 0; attempt < 3 && (status === 0 || status >= 400); attempt++) {
1512
+ try {
1513
+ const ctrl = new AbortController();
1514
+ const t = setTimeout(() => ctrl.abort(), 15000);
1515
+ const res = await fetch(liveUrl, { method: 'GET', redirect: 'follow', signal: ctrl.signal });
1516
+ clearTimeout(t);
1517
+ status = res.status;
1518
+ }
1519
+ catch {
1520
+ status = 0;
1521
+ }
1522
+ if (status === 0 || status >= 400)
1523
+ await new Promise((r) => setTimeout(r, 5000));
1524
+ }
1525
+ if (status === 0 || status >= 400) {
1526
+ return {
1527
+ ok: false,
1528
+ commitHash,
1529
+ branch,
1530
+ deployedNote: '',
1531
+ errorDetail: `Deployment finished, but ${liveUrl} returned ${status === 0 ? 'no response' : `HTTP ${status}`}.`,
1532
+ };
1533
+ }
1534
+ actions.push(`verified live at ${liveUrl} (HTTP ${status})`);
1535
+ }
1536
+ const deployedNote = actions.length > 0
1537
+ ? `🚀 **Deployed**: ${actions.join(', ')}.`
1538
+ : `🚀 **Deployed**: commit \`${commitHash}\` on \`${branch}\`.`;
1539
+ return { ok: true, commitHash, branch, deployedNote, errorDetail: '' };
1345
1540
  }
1541
+ /**
1542
+ * Deploy a card the human approved into Done (or flagged as `deploying`).
1543
+ * Idempotent: keyed on card ID only (never on `updated_at`, which changes whenever we post a comment),
1544
+ * so a card is deployed at most once unless the human explicitly re-triggers it into `deploying`.
1545
+ */
1346
1546
  export async function deployApprovedCardIfNeeded(client, card, projectPath) {
1347
1547
  if (!projectPath || !fs.existsSync(projectPath))
1348
1548
  return false;
1349
1549
  const shortId = card.id.slice(0, 8);
1350
1550
  const deployLock = path.join(getAgileMonsterLocksDir(), `deployed-${shortId}.json`);
1351
- const cardUpdatedAt = card.updated_at || '';
1551
+ let existing = null;
1352
1552
  if (fs.existsSync(deployLock)) {
1353
1553
  try {
1354
- const existing = JSON.parse(fs.readFileSync(deployLock, 'utf8'));
1355
- if (!cardUpdatedAt || existing?.card_updated_at === cardUpdatedAt) {
1356
- return false;
1357
- }
1554
+ existing = JSON.parse(fs.readFileSync(deployLock, 'utf8'));
1358
1555
  }
1359
1556
  catch {
1557
+ existing = { status: 'deployed' }; // unreadable lock: fail safe (never loop)
1558
+ }
1559
+ }
1560
+ const explicitRedeploy = card.agent_status === 'deploying';
1561
+ if (existing) {
1562
+ // Already handled and the human has not explicitly asked for a re-deploy
1563
+ if (!explicitRedeploy)
1360
1564
  return false;
1565
+ // Re-deploy request already being processed / processed for this exact request
1566
+ if (existing.redeploy_of === card.updated_at || existing.status === 'running') {
1567
+ const age = Date.now() - (Date.parse(existing.started_at || '') || 0);
1568
+ if (existing.status !== 'running' || age < 15 * 60 * 1000)
1569
+ return false;
1361
1570
  }
1362
1571
  }
1572
+ // Cards that were already deployed during the Review stage only need the Done confirmation, not another deploy
1573
+ const deployStage = (card.deploy_stage || 'review').toLowerCase();
1363
1574
  const provider = card.agent_provider || 'cursor';
1364
1575
  const rawText = `${card.title || ''} ${card.description || ''}`;
1365
1576
  const isContainerRestartCard = /\b(restart|redeploy|rebuild)\b.*\b(container|docker|server|prod)/i.test(rawText);
1577
+ const writeLock = (status, extra = {}) => {
1578
+ try {
1579
+ fs.writeFileSync(deployLock, JSON.stringify({
1580
+ card_id: card.id,
1581
+ status,
1582
+ title: card.title,
1583
+ redeploy_of: explicitRedeploy ? card.updated_at : null,
1584
+ started_at: new Date().toISOString(),
1585
+ ...extra,
1586
+ }), 'utf8');
1587
+ }
1588
+ catch { }
1589
+ };
1590
+ writeLock('running');
1366
1591
  try {
1367
- fs.writeFileSync(deployLock, JSON.stringify({
1368
- card_id: card.id,
1369
- card_updated_at: cardUpdatedAt,
1370
- title: card.title,
1371
- started_at: new Date().toISOString(),
1372
- }), 'utf8');
1373
- const res = runProjectDeploymentPipeline(projectPath, card.title || 'update', shortId, isContainerRestartCard);
1374
- const msg = res.deployedNote
1375
- ? `${res.deployedNote} Confirmed and moved card to **Done**.`
1376
- : `🚀 **Deployed**: Confirmed latest commit \`${res.commitHash}\` is live on \`origin/${res.branch}\` and moved card to **Done**.`;
1592
+ if (deployStage === 'manual') {
1593
+ writeLock('deployed', { note: 'manual deploy stage' });
1594
+ return false;
1595
+ }
1596
+ if (deployStage === 'review' && !explicitRedeploy && !isContainerRestartCard) {
1597
+ // Already deployed + verified before moving to Testing — nothing more to run.
1598
+ writeLock('deployed', { note: 'deployed at review stage' });
1599
+ return false;
1600
+ }
1601
+ const res = await runProjectDeploymentPipeline(projectPath, card.title || 'update', shortId, isContainerRestartCard, card);
1602
+ if (!res.ok) {
1603
+ writeLock('failed', { error: res.errorDetail });
1604
+ await client.blockTask(card.id, provider, `⚠️ Deployment could not be verified, so this card was NOT marked as deployed.\n\nWhat failed: ${res.errorDetail}\n\nFix the issue and move the card back to Done (or set it to Deploying) to retry.`);
1605
+ return false;
1606
+ }
1607
+ const msg = `${res.deployedNote} Confirmed and moved card to **Done**.`;
1377
1608
  await client.completeTask(card.id, provider, msg);
1609
+ writeLock('deployed', { commit: res.commitHash, completed_at: new Date().toISOString() });
1378
1610
  appendIdeActivityLog(projectPath, {
1379
1611
  cardId: card.id,
1380
1612
  cardTitle: card.title,
@@ -1383,18 +1615,60 @@ export async function deployApprovedCardIfNeeded(client, card, projectPath) {
1383
1615
  status: 'done',
1384
1616
  summary: msg,
1385
1617
  });
1386
- sendDesktopNotification(`Agile Monster -> Production Confirmed`, `Deployed "${card.title}" (${res.commitHash}) and moved to Done!`);
1618
+ sendDesktopNotification(`Agile Monster -> Production Confirmed`, `Deployed "${card.title}" (${res.commitHash})`);
1387
1619
  return true;
1388
1620
  }
1389
1621
  catch (err) {
1390
- try {
1391
- await client.addProgress(card.id, provider, `⚠️ **[Auto-Deploy Note]** Attempted to deploy approved changes in \`${projectPath}\`: ${err?.message || String(err)}`);
1392
- }
1393
- catch { }
1622
+ writeLock('failed', { error: String(err?.message || err).slice(0, 500) });
1394
1623
  return false;
1395
1624
  }
1396
1625
  }
1397
- export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
1626
+ /**
1627
+ * Machine-wide single-worker election. Only ONE process per machine may claim, edit, or deploy cards.
1628
+ * IDE-spawned MCP instances (Cursor, Claude Code, Antigravity, …) can be long-lived and run stale code,
1629
+ * so they defer to the background pulse daemon (role 'daemon' always wins) or to any live, fresh holder.
1630
+ */
1631
+ const WORKER_LOCK_STALE_MS = 75 * 1000;
1632
+ function isPidAlive(pid) {
1633
+ try {
1634
+ process.kill(pid, 0);
1635
+ return true;
1636
+ }
1637
+ catch {
1638
+ return false;
1639
+ }
1640
+ }
1641
+ export function acquireWorkerLock(role) {
1642
+ const lockPath = path.join(os.homedir(), '.agilemonster', 'worker.lock.json');
1643
+ try {
1644
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
1645
+ let holder = null;
1646
+ if (fs.existsSync(lockPath)) {
1647
+ try {
1648
+ holder = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
1649
+ }
1650
+ catch {
1651
+ holder = null;
1652
+ }
1653
+ }
1654
+ const holderLive = !!holder?.pid &&
1655
+ holder.pid !== process.pid &&
1656
+ isPidAlive(holder.pid) &&
1657
+ Date.now() - (holder.heartbeat || 0) < WORKER_LOCK_STALE_MS;
1658
+ if (holderLive) {
1659
+ const holderIsDaemon = holder?.role === 'daemon';
1660
+ // A daemon only yields to another live daemon; stdio instances always yield to a live holder.
1661
+ if (role === 'stdio' || holderIsDaemon)
1662
+ return false;
1663
+ }
1664
+ fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, role, version: MCP_SERVER_VERSION, heartbeat: Date.now() }), 'utf8');
1665
+ return true;
1666
+ }
1667
+ catch {
1668
+ return role === 'daemon';
1669
+ }
1670
+ }
1671
+ export function startBuiltInPullPulse(server, client, intervalMs = 20000, role = 'stdio') {
1398
1672
  const notifiedCardIds = new Set();
1399
1673
  let updateTriggered = false;
1400
1674
  let pulseCount = 0;
@@ -1490,28 +1764,31 @@ export function startBuiltInPullPulse(server, client, intervalMs = 20000) {
1490
1764
  catch { }
1491
1765
  }
1492
1766
  }
1493
- // Zero-touch autonomous execution for any queued cards bound to local folders on this machine
1494
- const boundQueued = res.all_bound_queued_cards || [];
1495
- for (const qCard of boundQueued) {
1496
- const targetPath = (qCard.bound_project_path || '').trim() ||
1497
- (qCard.board_id === config.board_id ? config.project_path || process.cwd() : '');
1498
- if (targetPath && fs.existsSync(targetPath)) {
1499
- await executeQueuedCardAutonomously(client, qCard, targetPath);
1767
+ // Zero-touch autonomous execution & deployment — ONLY the elected machine-wide worker may do this
1768
+ const isWorker = acquireWorkerLock(role);
1769
+ if (isWorker) {
1770
+ const boundQueued = res.all_bound_queued_cards || [];
1771
+ for (const qCard of boundQueued) {
1772
+ const targetPath = (qCard.bound_project_path || '').trim() ||
1773
+ (qCard.board_id === config.board_id ? config.project_path || process.cwd() : '');
1774
+ if (targetPath && fs.existsSync(targetPath)) {
1775
+ await executeQueuedCardAutonomously(client, qCard, targetPath);
1776
+ }
1500
1777
  }
1501
- }
1502
- // Zero-touch auto-commit & push (deploy to production) for cards approved into Production / Done!
1503
- const boundDone = res.all_bound_done_cards || [];
1504
- for (const dCard of boundDone) {
1505
- const targetPath = (dCard.bound_project_path || '').trim() ||
1506
- (dCard.board_id === config.board_id ? config.project_path || process.cwd() : '');
1507
- if (targetPath && fs.existsSync(targetPath)) {
1508
- await deployApprovedCardIfNeeded(client, dCard, targetPath);
1778
+ const boundDone = res.all_bound_done_cards || [];
1779
+ for (const dCard of boundDone) {
1780
+ const targetPath = (dCard.bound_project_path || '').trim() ||
1781
+ (dCard.board_id === config.board_id ? config.project_path || process.cwd() : '');
1782
+ if (targetPath && fs.existsSync(targetPath)) {
1783
+ await deployApprovedCardIfNeeded(client, dCard, targetPath);
1784
+ }
1509
1785
  }
1510
1786
  }
1511
1787
  if ((res.version?.force_update_requested || res.version?.update_available) &&
1512
- !updateTriggered) {
1788
+ !updateTriggered &&
1789
+ role === 'daemon') {
1513
1790
  updateTriggered = true;
1514
- triggerBackgroundSelfUpdate();
1791
+ triggerBackgroundSelfUpdate(res.version?.latest_version);
1515
1792
  }
1516
1793
  }
1517
1794
  catch {
@@ -1528,25 +1805,20 @@ export async function runStdioServer() {
1528
1805
  const server = createServer(client);
1529
1806
  const transport = new StdioServerTransport();
1530
1807
  await server.connect(transport);
1531
- startBuiltInPullPulse(server, client, 20000);
1808
+ startBuiltInPullPulse(server, client, 20000, 'stdio');
1532
1809
  }
1533
1810
  export async function runBackgroundPulseDaemon(intervalMs = 20000) {
1534
1811
  const client = new AgileMonsterClient();
1535
1812
  console.log(`[Agile Monster Pulse Daemon v${MCP_SERVER_VERSION}] Listening for queued agent tasks across all bound repositories (interval: ${intervalMs / 1000}s)...`);
1536
- startBuiltInPullPulse(null, client, intervalMs);
1813
+ startBuiltInPullPulse(null, client, intervalMs, 'daemon');
1537
1814
  const selfScript = process.argv[1] || '';
1538
1815
  const initialMtime = selfScript && fs.existsSync(selfScript) ? fs.statSync(selfScript).mtimeMs : 0;
1539
- // Auto-restart daemon if mcp-server binary/dist on disk is updated so it never runs stale code in memory
1816
+ // Auto-restart daemon if mcp-server binary/dist on disk is updated (launchd KeepAlive respawns it cleanly)
1540
1817
  setInterval(() => {
1541
1818
  try {
1542
1819
  if (selfScript && initialMtime > 0 && fs.existsSync(selfScript)) {
1543
1820
  const curMtime = fs.statSync(selfScript).mtimeMs;
1544
1821
  if (curMtime > initialMtime + 1000) {
1545
- const child = spawn(process.execPath, process.argv.slice(1), {
1546
- detached: true,
1547
- stdio: 'ignore',
1548
- });
1549
- child.unref();
1550
1822
  process.exit(0);
1551
1823
  }
1552
1824
  }
package/dist/types.d.ts CHANGED
@@ -34,6 +34,12 @@ export interface Card {
34
34
  updated_at?: string;
35
35
  bound_project_path?: string | null;
36
36
  bound_project_name?: string | null;
37
+ fallback_model?: string | null;
38
+ build_command?: string | null;
39
+ deploy_method?: string | null;
40
+ deploy_command?: string | null;
41
+ live_url?: string | null;
42
+ deploy_stage?: string | null;
37
43
  }
38
44
  export interface Board {
39
45
  id: string;
@@ -63,9 +69,17 @@ export interface BoardAgentConfig {
63
69
  custom_name?: string;
64
70
  short_name?: string | null;
65
71
  display_name?: string;
72
+ preferred_model?: string;
73
+ fallback_model?: string | null;
74
+ is_favorite?: boolean;
66
75
  project_name?: string | null;
67
76
  project_path?: string | null;
68
77
  git_remote?: string | null;
78
+ build_command?: string | null;
79
+ deploy_method?: string | null;
80
+ deploy_command?: string | null;
81
+ live_url?: string | null;
82
+ deploy_stage?: string | null;
69
83
  mcp_version?: string | null;
70
84
  connected?: boolean;
71
85
  last_connected_at?: string | null;
@@ -83,6 +97,10 @@ export interface DiscoveredProject {
83
97
  name: string;
84
98
  path: string;
85
99
  git_remote?: string | null;
100
+ build_command?: string | null;
101
+ deploy_method?: string | null;
102
+ deploy_command?: string | null;
103
+ live_url?: string | null;
86
104
  }
87
105
  export interface PulseResponse {
88
106
  board_id: string;
@@ -99,6 +117,13 @@ export interface PulseResponse {
99
117
  provider: string;
100
118
  project_name: string;
101
119
  project_path?: string | null;
120
+ preferred_model?: string | null;
121
+ fallback_model?: string | null;
122
+ build_command?: string | null;
123
+ deploy_method?: string | null;
124
+ deploy_command?: string | null;
125
+ live_url?: string | null;
126
+ deploy_stage?: string | null;
102
127
  }>;
103
128
  }
104
129
  export interface AutonomousEdit {
@@ -111,6 +136,8 @@ export interface AutonomousEdit {
111
136
  export interface AutonomousStepResponse {
112
137
  card_id: string;
113
138
  action?: 'select_files' | 'generate_edits';
139
+ needs_clarification?: boolean;
140
+ clarification_question?: string;
114
141
  selected_files?: string[];
115
142
  target_files?: string[];
116
143
  edits?: AutonomousEdit[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agilemonster/mcp-server",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "type": "module",
5
5
  "description": "Official Model Context Protocol (MCP) server & Orchestrator for Agile Monster (Cursor, Claude Code, Antigravity, Codex, Hermes, Windsurf)",
6
6
  "main": "dist/server.js",