aem-ext-daemon 0.4.2 → 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.
@@ -17,5 +17,10 @@ export declare function readFile(filePath: string): string;
17
17
  export declare function mkdir(dirPath: string): string;
18
18
  /** Write content to a file. Creates parent directories if needed. */
19
19
  export declare function writeFile(filePath: string, content: string): string;
20
+ /**
21
+ * Find all files recursively and return as flat JSON array of relative paths.
22
+ * Used for file search and @ mention autocomplete.
23
+ */
24
+ export declare function findFiles(dirPath: string, maxDepth?: number): string;
20
25
  /** Recursive directory listing up to a max depth. */
21
26
  export declare function listRecursive(dirPath: string, maxDepth?: number): string;
@@ -65,6 +65,43 @@ export function writeFile(filePath, content) {
65
65
  fs.writeFileSync(filePath, content, "utf-8");
66
66
  return `Written ${content.length} bytes to ${filePath}`;
67
67
  }
68
+ /**
69
+ * Find all files recursively and return as flat JSON array of relative paths.
70
+ * Used for file search and @ mention autocomplete.
71
+ */
72
+ export function findFiles(dirPath, maxDepth = 8) {
73
+ const SKIP = new Set([
74
+ "node_modules", "__pycache__", "dist", "build", ".git",
75
+ ".next", ".cache", "coverage", ".turbo", ".parcel-cache",
76
+ ]);
77
+ const MAX_FILES = 5000;
78
+ const files = [];
79
+ function walk(dir, rel, depth) {
80
+ if (depth > maxDepth || files.length >= MAX_FILES)
81
+ return;
82
+ let entries;
83
+ try {
84
+ entries = fs.readdirSync(dir, { withFileTypes: true });
85
+ }
86
+ catch {
87
+ return;
88
+ }
89
+ const filtered = entries.filter((e) => !e.name.startsWith(".") && !SKIP.has(e.name));
90
+ for (const entry of filtered) {
91
+ const relPath = rel ? `${rel}/${entry.name}` : entry.name;
92
+ if (entry.isDirectory()) {
93
+ walk(path.join(dir, entry.name), relPath, depth + 1);
94
+ }
95
+ else {
96
+ files.push(relPath);
97
+ if (files.length >= MAX_FILES)
98
+ return;
99
+ }
100
+ }
101
+ }
102
+ walk(dirPath, "", 0);
103
+ return JSON.stringify(files);
104
+ }
68
105
  /** Recursive directory listing up to a max depth. */
69
106
  export function listRecursive(dirPath, maxDepth = 3) {
70
107
  const result = [];
@@ -0,0 +1,38 @@
1
+ /**
2
+ * MCP capabilities — manage stdio-based MCP server processes on the local machine.
3
+ *
4
+ * Each server is a child process communicating via stdin/stdout using the MCP protocol.
5
+ * The daemon manages the lifecycle and proxies tool calls from the cloud server.
6
+ */
7
+ /**
8
+ * Start a stdio MCP server process and discover its tools.
9
+ *
10
+ * @param config Server configuration
11
+ * @returns JSON string with status and tool list
12
+ */
13
+ export declare function startServer(config: {
14
+ id: string;
15
+ command: string;
16
+ args?: string[];
17
+ env?: Record<string, string>;
18
+ }): Promise<string>;
19
+ /**
20
+ * Stop a running MCP server and kill its process.
21
+ */
22
+ export declare function stopServer(id: string): Promise<string>;
23
+ /**
24
+ * Call a tool on a running MCP server.
25
+ */
26
+ export declare function callTool(serverId: string, toolName: string, args: Record<string, unknown>): Promise<string>;
27
+ /**
28
+ * List tools for a running MCP server.
29
+ */
30
+ export declare function listTools(serverId: string): string;
31
+ /**
32
+ * Get status of all running MCP servers.
33
+ */
34
+ export declare function getStatus(): string;
35
+ /**
36
+ * Stop all running MCP servers. Called during daemon shutdown.
37
+ */
38
+ export declare function stopAll(): Promise<void>;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * MCP capabilities — manage stdio-based MCP server processes on the local machine.
3
+ *
4
+ * Each server is a child process communicating via stdin/stdout using the MCP protocol.
5
+ * The daemon manages the lifecycle and proxies tool calls from the cloud server.
6
+ */
7
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
9
+ import { logInfo, logError } from "../logger.js";
10
+ // Active MCP server processes, keyed by server ID
11
+ const servers = new Map();
12
+ /**
13
+ * Start a stdio MCP server process and discover its tools.
14
+ *
15
+ * @param config Server configuration
16
+ * @returns JSON string with status and tool list
17
+ */
18
+ export async function startServer(config) {
19
+ const { id, command, args, env } = config;
20
+ // Stop existing instance if running
21
+ if (servers.has(id)) {
22
+ await stopServer(id);
23
+ }
24
+ logInfo(`MCP: starting server "${id}" — ${command} ${(args || []).join(" ")}`);
25
+ const transport = new StdioClientTransport({
26
+ command,
27
+ args: args || [],
28
+ env: env ? { ...process.env, ...env } : undefined,
29
+ });
30
+ const client = new Client({
31
+ name: "aem-ext-daemon",
32
+ version: "1.0.0",
33
+ });
34
+ try {
35
+ await client.connect(transport);
36
+ // Discover tools
37
+ const result = await client.listTools();
38
+ const tools = (result.tools || []).map((t) => ({
39
+ name: t.name,
40
+ description: t.description || "",
41
+ inputSchema: t.inputSchema || { type: "object", properties: {} },
42
+ }));
43
+ // Try to get the PID from the transport's subprocess
44
+ let pid;
45
+ try {
46
+ // The StdioClientTransport exposes the child process internally
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ const tp = transport;
49
+ pid = tp._process?.pid || tp.process?.pid;
50
+ }
51
+ catch {
52
+ // PID not available — not critical
53
+ }
54
+ const managed = { id, client, transport, tools, pid };
55
+ servers.set(id, managed);
56
+ logInfo(`MCP: server "${id}" started — ${tools.length} tools discovered${pid ? ` (PID ${pid})` : ""}`);
57
+ return JSON.stringify({
58
+ running: true,
59
+ serverId: id,
60
+ pid,
61
+ toolCount: tools.length,
62
+ tools: tools.map((t) => ({ name: t.name, description: t.description })),
63
+ });
64
+ }
65
+ catch (err) {
66
+ const message = err instanceof Error ? err.message : String(err);
67
+ logError("mcp:start", message, 0);
68
+ // Ensure cleanup on failure
69
+ try {
70
+ await client.close();
71
+ }
72
+ catch { /* ignore */ }
73
+ return JSON.stringify({
74
+ running: false,
75
+ serverId: id,
76
+ error: message,
77
+ });
78
+ }
79
+ }
80
+ /**
81
+ * Stop a running MCP server and kill its process.
82
+ */
83
+ export async function stopServer(id) {
84
+ const server = servers.get(id);
85
+ if (!server) {
86
+ return JSON.stringify({ stopped: true, serverId: id, wasRunning: false });
87
+ }
88
+ logInfo(`MCP: stopping server "${id}"`);
89
+ try {
90
+ await server.client.close();
91
+ }
92
+ catch {
93
+ // Force-kill if close doesn't work
94
+ if (server.pid) {
95
+ try {
96
+ process.kill(server.pid, "SIGTERM");
97
+ }
98
+ catch { /* already dead */ }
99
+ }
100
+ }
101
+ servers.delete(id);
102
+ return JSON.stringify({ stopped: true, serverId: id, wasRunning: true });
103
+ }
104
+ /**
105
+ * Call a tool on a running MCP server.
106
+ */
107
+ export async function callTool(serverId, toolName, args) {
108
+ const server = servers.get(serverId);
109
+ if (!server) {
110
+ throw new Error(`MCP server "${serverId}" is not running`);
111
+ }
112
+ const result = await server.client.callTool({
113
+ name: toolName,
114
+ arguments: args,
115
+ });
116
+ // Extract text from content blocks
117
+ if (result.content && Array.isArray(result.content)) {
118
+ return result.content
119
+ .map((block) => {
120
+ if (block.type === "text")
121
+ return block.text || "";
122
+ return JSON.stringify(block);
123
+ })
124
+ .join("\n");
125
+ }
126
+ return JSON.stringify(result);
127
+ }
128
+ /**
129
+ * List tools for a running MCP server.
130
+ */
131
+ export function listTools(serverId) {
132
+ const server = servers.get(serverId);
133
+ if (!server) {
134
+ return JSON.stringify({ serverId, running: false, tools: [] });
135
+ }
136
+ return JSON.stringify({
137
+ serverId,
138
+ running: true,
139
+ tools: server.tools,
140
+ });
141
+ }
142
+ /**
143
+ * Get status of all running MCP servers.
144
+ */
145
+ export function getStatus() {
146
+ const statuses = Array.from(servers.entries()).map(([id, server]) => ({
147
+ serverId: id,
148
+ running: true,
149
+ pid: server.pid,
150
+ toolCount: server.tools.length,
151
+ }));
152
+ return JSON.stringify({ servers: statuses });
153
+ }
154
+ /**
155
+ * Stop all running MCP servers. Called during daemon shutdown.
156
+ */
157
+ export async function stopAll() {
158
+ const ids = Array.from(servers.keys());
159
+ for (const id of ids) {
160
+ await stopServer(id);
161
+ }
162
+ }
package/dist/daemon.d.ts CHANGED
@@ -22,7 +22,7 @@ export declare class Daemon {
22
22
  /** Start the daemon. */
23
23
  start(): void;
24
24
  /** Stop the daemon gracefully. */
25
- stop(): void;
25
+ stop(): Promise<void>;
26
26
  private handleStatus;
27
27
  private handleMessage;
28
28
  private handleNeedsPairing;
package/dist/daemon.js CHANGED
@@ -17,6 +17,7 @@ import { DaemonConnection } from "./connection.js";
17
17
  import { ensureIdentity, getWorkspaceRoot, setWorkspaceRoot } from "./identity.js";
18
18
  import { generatePairCode, displayPairCode } from "./pairing.js";
19
19
  import { dispatch } from "./dispatcher.js";
20
+ import { stopAll as stopAllMcpServers } from "./capabilities/mcp.js";
20
21
  import { getLogFilePath } from "./logger.js";
21
22
  // Read version from package.json so it stays in sync automatically
22
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -64,8 +65,9 @@ export class Daemon {
64
65
  this.connection.connect();
65
66
  }
66
67
  /** Stop the daemon gracefully. */
67
- stop() {
68
+ async stop() {
68
69
  console.log("\n Shutting down...");
70
+ await stopAllMcpServers();
69
71
  this.connection.disconnect();
70
72
  }
71
73
  handleStatus(status) {
@@ -11,6 +11,7 @@ import * as fsCap from "./capabilities/fs.js";
11
11
  import * as gitCap from "./capabilities/git.js";
12
12
  import * as shellCap from "./capabilities/shell.js";
13
13
  import * as skillsCap from "./capabilities/skills.js";
14
+ import * as mcpCap from "./capabilities/mcp.js";
14
15
  /**
15
16
  * Validate that a given path is within the workspace root.
16
17
  * Prevents path traversal attacks.
@@ -77,6 +78,12 @@ async function dispatchInner(command, payload, connection) {
77
78
  : getWorkspaceRoot();
78
79
  return fsCap.listRecursive(target, payload.depth);
79
80
  }
81
+ case "fs:find": {
82
+ const target = payload.path
83
+ ? validatePath(payload.path)
84
+ : getWorkspaceRoot();
85
+ return fsCap.findFiles(target, payload.depth);
86
+ }
80
87
  // ─── Workspace ──────────────────────────────────────
81
88
  case "workspace:get": {
82
89
  return { workspaceRoot: getWorkspaceRoot() };
@@ -133,6 +140,27 @@ async function dispatchInner(command, payload, connection) {
133
140
  throw new Error("No workspace root configured.");
134
141
  return skillsCap.seedSkills(workspace);
135
142
  }
143
+ // ─── MCP Servers ─────────────────────────────────────
144
+ case "mcp:start": {
145
+ return mcpCap.startServer({
146
+ id: payload.id,
147
+ command: payload.command,
148
+ args: payload.args,
149
+ env: payload.env,
150
+ });
151
+ }
152
+ case "mcp:stop": {
153
+ return mcpCap.stopServer(payload.serverId);
154
+ }
155
+ case "mcp:call": {
156
+ return mcpCap.callTool(payload.serverId, payload.toolName, payload.args || {});
157
+ }
158
+ case "mcp:tools": {
159
+ return mcpCap.listTools(payload.serverId);
160
+ }
161
+ case "mcp:status": {
162
+ return mcpCap.getStatus();
163
+ }
136
164
  default:
137
165
  throw new Error(`Unknown command: ${command}`);
138
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aem-ext-daemon",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Local daemon for AEM Extension Builder — connects your machine to the cloud UI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,6 +15,7 @@
15
15
  "prepublishOnly": "npm run build"
16
16
  },
17
17
  "dependencies": {
18
+ "@modelcontextprotocol/sdk": "^1.29.0",
18
19
  "conf": "^13.0.1",
19
20
  "qrcode-terminal": "^0.12.0",
20
21
  "ws": "^8.18.0"