@bachbntdev/devcmd 0.0.0-stage → 2.7.1

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.
Files changed (66) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +849 -2
  3. package/dev +33 -0
  4. package/package.json +47 -4
  5. package/src/cli.js +217 -0
  6. package/src/commands/completion.js +97 -0
  7. package/src/commands/devices.js +26 -0
  8. package/src/commands/docker.js +35 -0
  9. package/src/commands/env.js +52 -0
  10. package/src/commands/frameworks.js +36 -0
  11. package/src/commands/history.js +59 -0
  12. package/src/commands/inspect.js +48 -0
  13. package/src/commands/lifecycle.js +70 -0
  14. package/src/config/index.js +35 -0
  15. package/src/config/user.js +101 -0
  16. package/src/constants.js +23 -0
  17. package/src/devices/android.js +89 -0
  18. package/src/devices/index.js +14 -0
  19. package/src/devices/ios.js +47 -0
  20. package/src/generators/clean-project.js +52 -0
  21. package/src/generators/python-project.js +175 -0
  22. package/src/generators/reset-project.js +16 -0
  23. package/src/handlers/clone.js +73 -0
  24. package/src/handlers/devices.js +78 -0
  25. package/src/handlers/history.js +75 -0
  26. package/src/handlers/json.js +68 -0
  27. package/src/handlers/lifecycle.js +119 -0
  28. package/src/handlers/mcp.js +10 -0
  29. package/src/handlers/scaffold.js +166 -0
  30. package/src/handlers/system.js +135 -0
  31. package/src/handlers/tools.js +66 -0
  32. package/src/handlers/workflow.js +121 -0
  33. package/src/mcp/processes.js +164 -0
  34. package/src/mcp/server.js +32 -0
  35. package/src/mcp/tools.js +389 -0
  36. package/src/openers/built-ins.json +80 -0
  37. package/src/openers/engine.js +51 -0
  38. package/src/openers/index.js +26 -0
  39. package/src/openers/loader.js +89 -0
  40. package/src/openers/schema.js +81 -0
  41. package/src/projects/adapters/android.js +24 -0
  42. package/src/projects/adapters/common.js +24 -0
  43. package/src/projects/adapters/flutter.js +29 -0
  44. package/src/projects/adapters/index.js +30 -0
  45. package/src/projects/adapters/ios.js +35 -0
  46. package/src/projects/adapters/node.js +112 -0
  47. package/src/projects/adapters/python.js +90 -0
  48. package/src/projects/adapters/recipe.js +13 -0
  49. package/src/projects/detect.js +187 -0
  50. package/src/projects/profile.js +175 -0
  51. package/src/projects/recent.js +50 -0
  52. package/src/projects/workspaces.js +96 -0
  53. package/src/recipes/action-runner.js +24 -0
  54. package/src/recipes/built-ins.json +417 -0
  55. package/src/recipes/engine.js +159 -0
  56. package/src/recipes/index.js +34 -0
  57. package/src/recipes/loader.js +82 -0
  58. package/src/recipes/schema.js +225 -0
  59. package/src/runner/captured.js +125 -0
  60. package/src/runner/check.js +93 -0
  61. package/src/runner/command.js +43 -0
  62. package/src/runner/parallel.js +110 -0
  63. package/src/runner/process.js +80 -0
  64. package/src/runtime/execute.js +99 -0
  65. package/src/utils/args.js +219 -0
  66. package/src/utils/interpolate.js +10 -0
@@ -0,0 +1,135 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const { CONFIG_FILE, OPENERS_DIR, PROJECTS_FILE, RECIPES_DIR, VERSION, frameworkDefinitions } = require('../config');
4
+ const { setConfigValue } = require('../config/user');
5
+ const { getExistingProjects } = require('../projects/recent');
6
+ const { checkTools } = require('../runner/check');
7
+ const { loadRecipeFile } = require('../recipes');
8
+ const { loadOpenerFile } = require('../openers');
9
+ const { executeCommands } = require('../runtime/execute');
10
+
11
+ const PACKAGE_SPEC = '@bachbntdev/devcmd@latest';
12
+
13
+ function printConfig(p, config) {
14
+ p.note(JSON.stringify(config, null, 2), `Config: ${CONFIG_FILE}`);
15
+ }
16
+
17
+ function handleConfig(context) {
18
+ const { p, positionals, config } = context;
19
+ if (positionals.length === 0 || (positionals[0] === 'show' && positionals.length === 1)) {
20
+ printConfig(p, config);
21
+ p.outro('Use: dev config set <key> <value>');
22
+ return 0;
23
+ }
24
+ if (positionals[0] === 'set' && positionals.length === 3) {
25
+ printConfig(p, setConfigValue(positionals[1], positionals[2]));
26
+ p.outro('Configuration saved.');
27
+ return 0;
28
+ }
29
+ throw new Error('Usage: dev config [show] or dev config set <key> <value>');
30
+ }
31
+
32
+ async function handleUpdate(context) {
33
+ const { p, pc, options } = context;
34
+ p.log.info(`Current version: ${VERSION}`);
35
+ return executeCommands(p, pc, [
36
+ { executable: 'npm', args: ['install', '--global', PACKAGE_SPEC] },
37
+ ], { command: 'update' }, options);
38
+ }
39
+
40
+ function handleDoctor(context) {
41
+ const { p, pc, config } = context;
42
+ const checks = checkTools(config);
43
+ p.note(checks.map((check) => {
44
+ const marker = check.found ? pc.green('OK') : check.required ? pc.red('MISSING') : pc.yellow('optional');
45
+ return `${marker.padEnd(18)} ${check.name.padEnd(18)} ${check.version || check.path || ''}`;
46
+ }).join('\n'), 'Development environment');
47
+ const hints = checks.filter((check) => !check.found && check.hint);
48
+ if (hints.length > 0) {
49
+ p.note(hints.map((check) => `${check.name.padEnd(18)} ${check.hint}`).join('\n'), 'How to install missing tools');
50
+ }
51
+ const missing = checks.filter((check) => check.required && !check.found);
52
+ p.outro(missing.length ? 'Required tools are missing.' : 'DevCmd is ready.');
53
+ return missing.length ? 1 : 0;
54
+ }
55
+
56
+ function handleProjects(context) {
57
+ const { p } = context;
58
+ const projects = getExistingProjects();
59
+ if (projects.length === 0) {
60
+ p.outro('No recent projects yet.');
61
+ return 0;
62
+ }
63
+ p.note(
64
+ projects.map((project, index) => `${index + 1}. ${project.name} [${project.type}]\n ${project.path}`).join('\n'),
65
+ `Recent projects: ${PROJECTS_FILE}`
66
+ );
67
+ p.outro(`${projects.length} project(s).`);
68
+ return 0;
69
+ }
70
+
71
+ function handleInfo(context) {
72
+ const { p, pc } = context;
73
+ const definitions = context.frameworkDefinitions || frameworkDefinitions;
74
+ p.note(
75
+ Object.entries(definitions)
76
+ .map(([name, definition]) => {
77
+ const source = definition.source === 'user' ? ' [user recipe]' : '';
78
+ return `${pc.bold(name)}: ${definition.description}${source}`;
79
+ })
80
+ .join('\n'),
81
+ 'Supported frameworks'
82
+ );
83
+ p.outro('Run dev help for usage and options.');
84
+ return 0;
85
+ }
86
+
87
+ function handleRecipes(context) {
88
+ const { p, positionals, recipeRegistry } = context;
89
+ if (positionals.length === 0 || (positionals[0] === 'list' && positionals.length === 1)) {
90
+ const lines = [...recipeRegistry.values()].map(
91
+ (recipe) => `${recipe.name.padEnd(20)} [${recipe.source}] ${recipe.description}`
92
+ );
93
+ p.note(lines.join('\n'), 'Available recipes');
94
+ p.outro(`User recipe directory: ${RECIPES_DIR}`);
95
+ return 0;
96
+ }
97
+ if (positionals[0] === 'validate' && positionals.length === 2) {
98
+ const recipes = loadRecipeFile(positionals[1], 'user');
99
+ p.note(recipes.map((recipe) => `${recipe.name}: ${recipe.description}`).join('\n'), 'Valid recipes');
100
+ p.outro(`${recipes.length} recipe(s) validated.`);
101
+ return 0;
102
+ }
103
+ throw new Error('Usage: dev recipes [list] or dev recipes validate <file>');
104
+ }
105
+
106
+ function handleOpeners(context) {
107
+ const { p, positionals, openerRegistry } = context;
108
+ if (positionals.length === 0 || (positionals[0] === 'list' && positionals.length === 1)) {
109
+ const lines = [...openerRegistry.values()].map((opener) => {
110
+ const aliases = opener.aliases?.length ? ` aliases: ${opener.aliases.join(', ')}` : '';
111
+ return `${opener.name.padEnd(20)} [${opener.source}] ${opener.description}${aliases}`;
112
+ });
113
+ p.note(lines.join('\n'), 'Available openers');
114
+ p.outro(`User opener directory: ${OPENERS_DIR}`);
115
+ return 0;
116
+ }
117
+ if (positionals[0] === 'validate' && positionals.length === 2) {
118
+ const openers = loadOpenerFile(positionals[1], 'user');
119
+ p.note(openers.map((opener) => `${opener.name}: ${opener.description}`).join('\n'), 'Valid openers');
120
+ p.outro(`${openers.length} opener(s) validated.`);
121
+ return 0;
122
+ }
123
+ throw new Error('Usage: dev openers [list] or dev openers validate <file>');
124
+ }
125
+
126
+ module.exports = {
127
+ handleConfig,
128
+ handleDoctor,
129
+ handleInfo,
130
+ handleOpeners,
131
+ handleProjects,
132
+ handleRecipes,
133
+ handleUpdate,
134
+ printConfig,
135
+ };
@@ -0,0 +1,66 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const path = require('path');
4
+ const { buildDockerCommands } = require('../commands/docker');
5
+ const { checkEnv, initEnv } = require('../commands/env');
6
+ const { detectProject } = require('../projects/detect');
7
+ const { executeCommands } = require('../runtime/execute');
8
+
9
+ function resolveRoot(projectPath, context) {
10
+ if (projectPath) return path.resolve(projectPath);
11
+ try {
12
+ return detectProject(process.cwd(), { registry: context.recipeRegistry }).root;
13
+ } catch {
14
+ return process.cwd();
15
+ }
16
+ }
17
+
18
+ function handleEnv(context) {
19
+ const { p, positionals, options } = context;
20
+ const subcommand = ['init', 'check'].includes(positionals[0]) ? positionals[0] : 'init';
21
+ const rest = ['init', 'check'].includes(positionals[0]) ? positionals.slice(1) : positionals;
22
+ if (rest.length > 1) throw new Error('Usage: dev env [init|check] [path]');
23
+ const root = resolveRoot(rest[0], context);
24
+
25
+ if (subcommand === 'check') {
26
+ const result = checkEnv(root);
27
+ if (result.ok) {
28
+ p.outro(`.env defines every variable from ${result.template}.`);
29
+ return 0;
30
+ }
31
+ p.note(result.missing.join('\n'), `Missing from .env (see ${result.template})`);
32
+ p.outro(`${result.missing.length} variable(s) missing.`);
33
+ return 1;
34
+ }
35
+ if (options.dryRun) {
36
+ p.outro(`Would create .env from the template in ${root} if it does not exist.`);
37
+ return 0;
38
+ }
39
+ const result = initEnv(root);
40
+ p.outro(result.created
41
+ ? `Created .env from ${result.template}. Fill in the values.`
42
+ : '.env already exists. Nothing was changed.');
43
+ return 0;
44
+ }
45
+
46
+ async function handleDocker(context) {
47
+ const { p, pc, positionals, options, config } = context;
48
+ if (positionals.length < 1 || positionals.length > 2) {
49
+ throw new Error('Usage: dev docker <up|down|build|ps|logs|restart> [path]');
50
+ }
51
+ const root = resolveRoot(positionals[1], context);
52
+ const commands = buildDockerCommands(positionals[0], root);
53
+ return executeCommands(p, pc, commands, {
54
+ command: 'docker',
55
+ action: positionals[0],
56
+ projectPath: root,
57
+ projectName: path.basename(root),
58
+ projectType: 'docker',
59
+ config,
60
+ }, { ...options, requirements: ['docker'] });
61
+ }
62
+
63
+ module.exports = {
64
+ handleDocker,
65
+ handleEnv,
66
+ };
@@ -0,0 +1,121 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const path = require('path');
4
+ const { buildLifecycleCommands, buildTaskCommands, listTasks } = require('../commands/lifecycle');
5
+ const { LIFECYCLE_COMMANDS } = require('../constants');
6
+ const { detectProject } = require('../projects/detect');
7
+ const { describeProfile, isTrusted, trustProfile } = require('../projects/profile');
8
+ const { formatSequence } = require('../runner/command');
9
+ const { runParallel } = require('../runner/parallel');
10
+ const { executeCommands } = require('../runtime/execute');
11
+
12
+ const TASK_NAME = /^[A-Za-z0-9][A-Za-z0-9:_.-]*$/;
13
+ const PARALLEL_ACTIONS = LIFECYCLE_COMMANDS.filter((action) => action !== 'open');
14
+
15
+ function detect(projectPath, context) {
16
+ return detectProject(projectPath || process.cwd(), { registry: context.recipeRegistry });
17
+ }
18
+
19
+ function projectMetadata(project, command, config) {
20
+ return {
21
+ command,
22
+ projectName: project.name,
23
+ projectPath: project.root,
24
+ projectType: project.type,
25
+ config,
26
+ };
27
+ }
28
+
29
+ async function handleTask(context) {
30
+ const { p, pc, positionals, options, config } = context;
31
+ if (positionals.length > 2) throw new Error('Usage: dev task [name] [path]');
32
+ const [name, projectPath] = positionals;
33
+ const project = detect(projectPath, context);
34
+ if (!name) {
35
+ const tasks = listTasks(project);
36
+ if (tasks.length === 0) {
37
+ p.outro('No tasks found. Add scripts to package.json or tasks to .devcmd.json.');
38
+ return 0;
39
+ }
40
+ p.note(
41
+ tasks.map((task) => {
42
+ const description = task.description ? ` ${pc.dim(task.description)}` : '';
43
+ return `${task.name.padEnd(20)} [${task.source}]${description}`;
44
+ }).join('\n'),
45
+ `Tasks: ${project.name}`
46
+ );
47
+ p.outro('Use: dev task <name> [path]');
48
+ return 0;
49
+ }
50
+ if (!TASK_NAME.test(name)) throw new Error(`Invalid task name: ${name}`);
51
+ const commands = buildTaskCommands(project, name, { config, filter: options.filter });
52
+ return executeCommands(p, pc, commands, projectMetadata(project, 'task', config), options);
53
+ }
54
+
55
+ async function handleParallel(context) {
56
+ const { p, pc, positionals, options, config } = context;
57
+ const [action, ...paths] = positionals;
58
+ if (!PARALLEL_ACTIONS.includes(action) || paths.length === 0) {
59
+ throw new Error(`Usage: dev parallel <${PARALLEL_ACTIONS.join('|')}> <path> [path...]`);
60
+ }
61
+ const jobs = paths.map((projectPath) => {
62
+ const project = detect(projectPath, context);
63
+ return {
64
+ label: project.name || path.basename(project.root),
65
+ commands: buildLifecycleCommands(action, project, { config }),
66
+ };
67
+ });
68
+ const labels = new Set(jobs.map((job) => job.label));
69
+ if (labels.size !== jobs.length) {
70
+ jobs.forEach((job, index) => {
71
+ job.label = `${job.label}#${index + 1}`;
72
+ });
73
+ }
74
+ for (const job of jobs) p.log.step(`${pc.cyan(job.label)}: ${pc.yellow(formatSequence(job.commands))}`);
75
+ if (options.dryRun) {
76
+ p.outro('No command was executed.');
77
+ return 0;
78
+ }
79
+ const result = await runParallel(jobs);
80
+ const summary = result.results
81
+ .map((entry) => `${entry.ok ? pc.green('✔') : pc.red('✖')} ${entry.label}${entry.ok ? '' : ` (exit ${entry.exitCode})`}`)
82
+ .join('\n');
83
+ p.note(summary, `dev ${action}`);
84
+ p.outro(result.ok ? `${pc.green('✔')} All projects completed.` : `${pc.red('✖')} Some projects failed.`);
85
+ return result.exitCode;
86
+ }
87
+
88
+ async function handleTrust(context) {
89
+ const { p, pc, positionals, options, config } = context;
90
+ if (positionals.length > 1) throw new Error('Usage: dev trust [path]');
91
+ const project = detect(positionals[0], context);
92
+ if (!project.profile) throw new Error(`No .devcmd.json was found for ${project.root}.`);
93
+ const entries = describeProfile(project, { config });
94
+ p.note(
95
+ entries.length
96
+ ? entries.map((entry) => `${pc.bold(entry.label)}\n ${formatSequence(entry.commands)}`).join('\n')
97
+ : 'The profile does not define any commands.',
98
+ project.profile.file
99
+ );
100
+ if (isTrusted(project.profile)) {
101
+ p.outro('This profile is already trusted.');
102
+ return 0;
103
+ }
104
+ if (!options.yes) {
105
+ const approved = await p.confirm({ message: 'Trust this profile and allow DevCmd to run these commands?' });
106
+ if (p.isCancel(approved) || !approved) {
107
+ p.cancel('Profile was not trusted.');
108
+ return 130;
109
+ }
110
+ }
111
+ trustProfile(project.profile);
112
+ p.outro('Profile trusted. It will need approval again if the file changes.');
113
+ return 0;
114
+ }
115
+
116
+ module.exports = {
117
+ PARALLEL_ACTIONS,
118
+ handleParallel,
119
+ handleTask,
120
+ handleTrust,
121
+ };
@@ -0,0 +1,164 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const { spawn } = require('child_process');
4
+ const { formatSequence } = require('../runner/command');
5
+ const { SIGNAL_EXIT_CODES } = require('../runner/process');
6
+
7
+ const DEFAULT_MAX_LOG_BYTES = 256 * 1024;
8
+ const DEFAULT_KILL_GRACE_MS = 3000;
9
+
10
+ class ProcessManager {
11
+ constructor(options = {}) {
12
+ this.spawnImpl = options.spawnImpl || spawn;
13
+ this.maxLogBytes = options.maxLogBytes || DEFAULT_MAX_LOG_BYTES;
14
+ this.killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
15
+ this.entries = new Map();
16
+ this.counter = 0;
17
+ }
18
+
19
+ start({ label, commands }) {
20
+ this.counter += 1;
21
+ const entry = {
22
+ id: `p${this.counter}`,
23
+ label,
24
+ commandLine: formatSequence(commands),
25
+ status: 'running',
26
+ exitCode: undefined,
27
+ startedAt: new Date().toISOString(),
28
+ log: '',
29
+ droppedChars: 0,
30
+ child: undefined,
31
+ stopRequested: false,
32
+ };
33
+ entry.done = new Promise((resolve) => {
34
+ entry.resolveDone = resolve;
35
+ });
36
+ this.entries.set(entry.id, entry);
37
+ this.runNext(entry, commands, 0);
38
+ return this.summary(entry);
39
+ }
40
+
41
+ runNext(entry, commands, index) {
42
+ if (entry.stopRequested) return this.finish(entry, 'stopped', undefined);
43
+ if (index >= commands.length) return this.finish(entry, 'exited', 0);
44
+ const command = commands[index];
45
+ let child;
46
+ try {
47
+ const spawnOptions = { shell: false, stdio: ['ignore', 'pipe', 'pipe'] };
48
+ if (command.cwd) spawnOptions.cwd = command.cwd;
49
+ child = this.spawnImpl(command.executable, command.args, spawnOptions);
50
+ } catch (error) {
51
+ this.append(entry, `${error.message}\n`);
52
+ return this.finish(entry, 'failed', error.code === 'ENOENT' ? 127 : 1);
53
+ }
54
+ entry.child = child;
55
+ child.stdout?.on('data', (data) => this.append(entry, data));
56
+ child.stderr?.on('data', (data) => this.append(entry, data));
57
+ let settled = false;
58
+ const settle = (callback) => {
59
+ if (settled) return;
60
+ settled = true;
61
+ entry.child = undefined;
62
+ callback();
63
+ };
64
+ child.once('error', (error) => settle(() => {
65
+ this.append(entry, `${error.message}\n`);
66
+ this.finish(entry, 'failed', error.code === 'ENOENT' ? 127 : 1);
67
+ }));
68
+ child.once('close', (code, signal) => settle(() => {
69
+ if (entry.stopRequested) this.finish(entry, 'stopped', code ?? SIGNAL_EXIT_CODES[signal]);
70
+ else if (signal) this.finish(entry, 'failed', SIGNAL_EXIT_CODES[signal] || 1);
71
+ else if (code === 0) this.runNext(entry, commands, index + 1);
72
+ else this.finish(entry, 'failed', code ?? 1);
73
+ }));
74
+ }
75
+
76
+ append(entry, data) {
77
+ entry.log += data.toString();
78
+ if (entry.log.length > this.maxLogBytes) {
79
+ const excess = entry.log.length - this.maxLogBytes;
80
+ entry.log = entry.log.slice(excess);
81
+ entry.droppedChars += excess;
82
+ }
83
+ }
84
+
85
+ finish(entry, status, exitCode) {
86
+ entry.status = status;
87
+ entry.exitCode = exitCode;
88
+ entry.endedAt = new Date().toISOString();
89
+ entry.resolveDone();
90
+ }
91
+
92
+ get(id) {
93
+ const entry = this.entries.get(id);
94
+ if (!entry) throw new Error(`Unknown process id: ${id}. Use list_processes to see running processes.`);
95
+ return entry;
96
+ }
97
+
98
+ summary(entry) {
99
+ return {
100
+ id: entry.id,
101
+ label: entry.label,
102
+ command: entry.commandLine,
103
+ status: entry.status,
104
+ exitCode: entry.exitCode,
105
+ startedAt: entry.startedAt,
106
+ endedAt: entry.endedAt,
107
+ };
108
+ }
109
+
110
+ list() {
111
+ return [...this.entries.values()].map((entry) => this.summary(entry));
112
+ }
113
+
114
+ logs(id, options = {}) {
115
+ const entry = this.get(id);
116
+ const tail = Number.isInteger(options.tail) && options.tail > 0 ? options.tail : 100;
117
+ const lines = entry.log.split('\n');
118
+ if (lines[lines.length - 1] === '') lines.pop();
119
+ return {
120
+ ...this.summary(entry),
121
+ truncated: entry.droppedChars > 0 || lines.length > tail,
122
+ output: lines.slice(-tail).join('\n'),
123
+ };
124
+ }
125
+
126
+ async stop(id) {
127
+ const entry = this.get(id);
128
+ if (entry.status !== 'running') return this.summary(entry);
129
+ entry.stopRequested = true;
130
+ const child = entry.child;
131
+ if (child) {
132
+ try {
133
+ child.kill('SIGTERM');
134
+ } catch {}
135
+ const timer = setTimeout(() => {
136
+ try {
137
+ if (entry.status === 'running') child.kill('SIGKILL');
138
+ } catch {}
139
+ }, this.killGraceMs);
140
+ timer.unref?.();
141
+ await Promise.race([
142
+ entry.done,
143
+ new Promise((resolve) => {
144
+ const wait = setTimeout(resolve, this.killGraceMs + 1000);
145
+ wait.unref?.();
146
+ }),
147
+ ]);
148
+ clearTimeout(timer);
149
+ }
150
+ return this.summary(entry);
151
+ }
152
+
153
+ stopAll() {
154
+ for (const entry of this.entries.values()) {
155
+ if (entry.status !== 'running') continue;
156
+ entry.stopRequested = true;
157
+ try {
158
+ entry.child?.kill('SIGTERM');
159
+ } catch {}
160
+ }
161
+ }
162
+ }
163
+
164
+ module.exports = { ProcessManager };
@@ -0,0 +1,32 @@
1
+ // Copyright (c) 2026 bachbnt
2
+
3
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
4
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
5
+ const {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ } = require('@modelcontextprotocol/sdk/types.js');
9
+ const { VERSION } = require('../config');
10
+ const { TOOLS, callTool, defaultProcesses } = require('./tools');
11
+
12
+ async function startMcpServer() {
13
+ const server = new Server(
14
+ { name: 'devcmd', version: VERSION },
15
+ { capabilities: { tools: {} } }
16
+ );
17
+
18
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
19
+
20
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
21
+ const { name, arguments: args = {} } = request.params;
22
+ return callTool(name, args);
23
+ });
24
+
25
+ server.onclose = () => defaultProcesses.stopAll();
26
+ process.once('exit', () => defaultProcesses.stopAll());
27
+
28
+ const transport = new StdioServerTransport();
29
+ await server.connect(transport);
30
+ }
31
+
32
+ module.exports = { TOOLS, callTool, startMcpServer };