@rse/ase 0.0.13 → 0.0.15

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/dst/ase-config.js CHANGED
@@ -324,16 +324,18 @@ export class Config {
324
324
  if (sc.kind === "default") {
325
325
  const doc = new Document();
326
326
  doc.contents = doc.createNode({});
327
- const preset = projectClassificationPresets.default;
328
- for (const [k, val] of Object.entries(preset)) {
329
- const segments = k.split(".");
330
- for (let j = 1; j < segments.length; j++) {
331
- const prefix = segments.slice(0, j);
332
- const node = doc.getIn(prefix, true);
333
- if (node === undefined)
334
- doc.setIn(prefix, doc.createNode({}));
327
+ if (this.name === "config") {
328
+ const preset = projectClassificationPresets.default;
329
+ for (const [k, val] of Object.entries(preset)) {
330
+ const segments = k.split(".");
331
+ for (let j = 1; j < segments.length; j++) {
332
+ const prefix = segments.slice(0, j);
333
+ const node = doc.getIn(prefix, true);
334
+ if (node === undefined)
335
+ doc.setIn(prefix, doc.createNode({}));
336
+ }
337
+ doc.setIn(segments, doc.createNode(val));
335
338
  }
336
- doc.setIn(segments, doc.createNode(val));
337
339
  }
338
340
  docs.push({ scope: sc, filename: "", doc });
339
341
  continue;
@@ -0,0 +1,222 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
+ */
6
+ import fs from "node:fs";
7
+ import { InvalidArgumentError } from "commander";
8
+ import { renderMermaidASCII } from "beautiful-mermaid";
9
+ /* custom argument parser for Commander: non-negative integer */
10
+ const parseInteger = (name) => (value) => {
11
+ const n = Number.parseInt(value, 10);
12
+ if (!Number.isFinite(n) || n < 0)
13
+ throw new InvalidArgumentError(`${name} must be a non-negative integer`);
14
+ return n;
15
+ };
16
+ /* custom argument parser for Commander: color mode */
17
+ const parseColorMode = (name) => (value) => {
18
+ if (value !== "none" && value !== "ansi16" && value !== "ansi256")
19
+ throw new InvalidArgumentError(`${name} must be "none", "ansi16", or "ansi256"`);
20
+ return value;
21
+ };
22
+ /* detect terminal column width */
23
+ const detectTermWidth = () => {
24
+ let width = 0;
25
+ /* attempt 1: query environment variable */
26
+ if (process.env.ASE_TERM_WIDTH !== undefined) {
27
+ const cols = Number.parseInt(process.env.ASE_TERM_WIDTH, 10);
28
+ if (Number.isFinite(cols) && cols > 0)
29
+ width = cols;
30
+ }
31
+ /* attempt 2: query stdout */
32
+ if (width === 0 && process.stdout.isTTY) {
33
+ const cols = process.stdout.columns;
34
+ if (typeof cols === "number" && cols > 0)
35
+ width = cols;
36
+ }
37
+ return width;
38
+ };
39
+ /* detect terminal row height */
40
+ const detectTermHeight = () => {
41
+ let height = 0;
42
+ /* attempt 1: query environment variable */
43
+ if (process.env.ASE_TERM_HEIGHT !== undefined) {
44
+ const rows = Number.parseInt(process.env.ASE_TERM_HEIGHT, 10);
45
+ if (Number.isFinite(rows) && rows > 0)
46
+ height = rows;
47
+ }
48
+ /* attempt 2: query stdout */
49
+ if (height === 0 && process.stdout.isTTY) {
50
+ const rows = process.stdout.rows;
51
+ if (typeof rows === "number" && rows > 0)
52
+ height = rows;
53
+ }
54
+ return height;
55
+ };
56
+ /* detect terminal color capability */
57
+ const detectColorMode = () => {
58
+ let mode = "none";
59
+ /* attempt 1: query environment variable (explicitly) */
60
+ if (process.env.ASE_TERM_COLORS !== undefined)
61
+ if (process.env.ASE_TERM_COLORS.match(/^(?:none|ansi16|ansi256)$/) !== null)
62
+ mode = process.env.ASE_TERM_COLORS;
63
+ /* attempt 2: query stdout */
64
+ if (mode === "none" && process.stdout.isTTY) {
65
+ const depth = process.stdout.getColorDepth();
66
+ if (depth >= 8)
67
+ mode = "ansi256";
68
+ else if (depth >= 4)
69
+ mode = "ansi16";
70
+ }
71
+ return mode;
72
+ };
73
+ /* truncate a single rendered line to a maximum visible column,
74
+ preserving ANSI escape sequences (CSI ...m) and appending an ANSI
75
+ reset sequence if any styling was active at the truncation point */
76
+ const truncateAnsiLine = (line, budget) => {
77
+ if (budget <= 0)
78
+ return "";
79
+ let out = "";
80
+ let visible = 0;
81
+ let styled = false;
82
+ let i = 0;
83
+ while (i < line.length) {
84
+ const ch = line[i];
85
+ if (ch === "\x1b" && line[i + 1] === "[") {
86
+ let j = i + 2;
87
+ while (j < line.length && !/[A-Za-z]/.test(line[j]))
88
+ j++;
89
+ if (j < line.length) {
90
+ const seq = line.slice(i, j + 1);
91
+ out += seq;
92
+ if (seq.endsWith("m")) {
93
+ const body = seq.slice(2, -1);
94
+ if (body === "" || body === "0")
95
+ styled = false;
96
+ else
97
+ styled = true;
98
+ }
99
+ i = j + 1;
100
+ continue;
101
+ }
102
+ i++;
103
+ continue;
104
+ }
105
+ if (visible >= budget)
106
+ break;
107
+ out += ch;
108
+ visible++;
109
+ i++;
110
+ }
111
+ if (styled)
112
+ out += "\x1b[0m";
113
+ return out;
114
+ };
115
+ /* pure rendering helper: turn a Mermaid source string plus options into
116
+ a rendered Unicode/ASCII diagram string. Throws on render failure. */
117
+ export const renderDiagram = (src, opts) => {
118
+ /* create diagram rendering */
119
+ let out = renderMermaidASCII(src, {
120
+ useAscii: opts.ascii,
121
+ paddingX: opts.nodeMarginX,
122
+ paddingY: opts.nodeMarginY,
123
+ boxBorderPadding: opts.nodePadding,
124
+ colorMode: opts.colorMode,
125
+ theme: opts.colorMode !== "none" ? {
126
+ fg: "#000000",
127
+ border: "#a0a0a0",
128
+ junction: "#a0a0a0",
129
+ arrow: "#404040",
130
+ line: "#707070",
131
+ corner: "#707070"
132
+ } : {
133
+ fg: "#000000",
134
+ border: "#000000",
135
+ junction: "#000000",
136
+ arrow: "#000000",
137
+ line: "#000000",
138
+ corner: "#000000"
139
+ }
140
+ });
141
+ /* optionally clip diagram rendering */
142
+ const termWidth = opts.terminalWidth;
143
+ const termHeight = opts.terminalHeight;
144
+ if (termWidth > 0 || termHeight > 0) {
145
+ const maxWidth = termWidth > 0 ? termWidth - opts.diagramClipX : 0;
146
+ const maxHeight = termHeight > 0 ? termHeight - opts.diagramClipY : 0;
147
+ const trailingNL = out.endsWith("\n");
148
+ let lines = (trailingNL ? out.slice(0, -1) : out).split("\n");
149
+ if (maxWidth > 0)
150
+ lines = lines.map((l) => truncateAnsiLine(l, maxWidth));
151
+ if (maxHeight > 0 && lines.length > maxHeight)
152
+ lines = lines.slice(0, maxHeight);
153
+ out = lines.join("\n") + (trailingNL ? "\n" : "");
154
+ }
155
+ return out;
156
+ };
157
+ /* read stdin into a single string */
158
+ const readStdin = async () => {
159
+ const chunks = [];
160
+ for await (const chunk of process.stdin)
161
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
162
+ return Buffer.concat(chunks).toString("utf8");
163
+ };
164
+ /* command-line handling */
165
+ export default class DiagramCommand {
166
+ log;
167
+ constructor(log) {
168
+ this.log = log;
169
+ }
170
+ /* register commands */
171
+ register(program) {
172
+ program
173
+ .command("diagram")
174
+ .description("Render Mermaid diagram specification as Unicode/ASCII art")
175
+ .option("-i, --input <file>", "read Mermaid source from file instead of stdin")
176
+ .option("-a, --ascii", "emit plain ASCII (+-|) instead of Unicode box-drawing", false)
177
+ .option("-c, --color-mode <mode>", "force color mode (\"none\", \"ansi16\", or \"ansi256\")", parseColorMode("--color-mode"), detectColorMode())
178
+ .option("--node-margin-x <n>", "horizontal margin between nodes of <n> characters", parseInteger("--node-margin-x"), 3)
179
+ .option("--node-margin-y <n>", "vertical margin between nodes of <n> lines", parseInteger("--node-margin-y"), 3)
180
+ .option("--node-padding <n>", "horizontal and vertical inner node padding with <n> characters", parseInteger("--node-padding"), 1)
181
+ .option("--diagram-clip-x <n>", "extra horizontal clipping of diagram to terminal width minus <n> characters", parseInteger("--diagram-clip-x"), 0)
182
+ .option("--diagram-clip-y <n>", "extra vertical clipping of diagram to terminal height minus <n> lines", parseInteger("--diagram-clip-y"), 0)
183
+ .option("--terminal-width <n>", "width of terminal of <n> characters (for diagram clipping)", parseInteger("--terminal-width"), detectTermWidth())
184
+ .option("--terminal-height <n>", "height of terminal of <n> lines (for diagram clipping)", parseInteger("--terminal-height"), detectTermHeight())
185
+ .action(async (opts) => {
186
+ /* fetch Mermaid diagram specification from stdin */
187
+ let src;
188
+ if (opts.input !== undefined)
189
+ src = fs.readFileSync(opts.input, "utf8");
190
+ else
191
+ src = await readStdin();
192
+ if (src.trim() === "") {
193
+ this.log.write("error", "diagram: empty Mermaid diagram specification");
194
+ process.exit(1);
195
+ }
196
+ /* create diagram rendering */
197
+ let out;
198
+ try {
199
+ out = renderDiagram(src, {
200
+ ascii: opts.ascii ?? false,
201
+ colorMode: opts.colorMode,
202
+ nodeMarginX: opts.nodeMarginX,
203
+ nodeMarginY: opts.nodeMarginY,
204
+ nodePadding: opts.nodePadding,
205
+ diagramClipX: opts.diagramClipX,
206
+ diagramClipY: opts.diagramClipY,
207
+ terminalWidth: opts.terminalWidth,
208
+ terminalHeight: opts.terminalHeight
209
+ });
210
+ }
211
+ catch (err) {
212
+ const message = err instanceof Error ? err.message : String(err);
213
+ this.log.write("error", `diagram: render failed: ${message}`);
214
+ process.exit(1);
215
+ }
216
+ /* output diagram rendering */
217
+ process.stdout.write(out);
218
+ if (!out.endsWith("\n"))
219
+ process.stdout.write("\n");
220
+ });
221
+ }
222
+ }
package/dst/ase-hook.js CHANGED
@@ -62,6 +62,40 @@ export default class HookCommand {
62
62
  }));
63
63
  return 0;
64
64
  }
65
+ /* handler for "ase hook pre-tool-use" */
66
+ doPreToolUse() {
67
+ /* read tool invocation information */
68
+ const stdin = fs.readFileSync(0, "utf8");
69
+ const input = stdin.trim() !== "" ? JSON.parse(stdin) : {};
70
+ /* determine whether to auto-approve the tool invocation */
71
+ const toolName = input.tool_name ?? "";
72
+ const toolInput = input.tool_input ?? {};
73
+ let approve = false;
74
+ let reason = "";
75
+ if (toolName === "Bash" && /^ase(\s|$)/.test(toolInput.command ?? "")) {
76
+ approve = true;
77
+ reason = "ASE CLI invocation auto-approved";
78
+ }
79
+ else if (toolName === "Skill" && /^(?:ase:)?ase-.+/.test(toolInput.skill ?? "")) {
80
+ approve = true;
81
+ reason = "ASE skill invocation auto-approved";
82
+ }
83
+ else if (/^mcp__plugin_ase_ase__.+/.test(toolName)) {
84
+ approve = true;
85
+ reason = "ASE MCP tool invocation auto-approved";
86
+ }
87
+ /* emit permission decision (or stay silent to defer to default flow) */
88
+ if (approve) {
89
+ process.stdout.write(JSON.stringify({
90
+ "hookSpecificOutput": {
91
+ "hookEventName": "PreToolUse",
92
+ "permissionDecision": "allow",
93
+ "permissionDecisionReason": reason
94
+ }
95
+ }));
96
+ }
97
+ return 0;
98
+ }
65
99
  /* register commands */
66
100
  register(program) {
67
101
  /* register CLI top-level command "ase hook" */
@@ -79,5 +113,12 @@ export default class HookCommand {
79
113
  .action(() => {
80
114
  process.exit(this.doSessionStart());
81
115
  });
116
+ /* register CLI sub-command "ase hook pre-tool-use" */
117
+ hookCmd
118
+ .command("pre-tool-use")
119
+ .description("handle Claude Code PreToolUse hook event")
120
+ .action(() => {
121
+ process.exit(this.doPreToolUse());
122
+ });
82
123
  }
83
124
  }
package/dst/ase-mcp.js ADDED
@@ -0,0 +1,156 @@
1
+ /*
2
+ ** Agentic Software Engineering (ASE)
3
+ ** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
+ ** Licensed under GPL 3.0 <https://spdx.org/licenses/GPL-3.0-only>
5
+ */
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import axios from "axios";
9
+ import * as v from "valibot";
10
+ import { execa } from "execa";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
13
+ import { Config, configSchema } from "./ase-config.js";
14
+ const HOST = "127.0.0.1";
15
+ /* schema for ".ase/service.yaml" (same shape as in ase-service.ts) */
16
+ const serviceSchema = v.nullish(v.strictObject({
17
+ port: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1024), v.maxValue(65535)))
18
+ }));
19
+ /* distinguish ECONNREFUSED from other Axios transport errors */
20
+ const isConnRefused = (err) => {
21
+ const e = err;
22
+ return e?.code === "ECONNREFUSED" || e?.cause?.code === "ECONNREFUSED";
23
+ };
24
+ /* probe the service and verify ASE identity banner */
25
+ const probe = async (port, projectId) => {
26
+ try {
27
+ const r = await axios.request({
28
+ method: "OPTIONS",
29
+ url: `http://${HOST}:${port}/`,
30
+ timeout: 2000,
31
+ validateStatus: () => true
32
+ });
33
+ if (r.status < 200 || r.status >= 300)
34
+ return false;
35
+ const d = r.data;
36
+ return d?.ase === true && d?.projectId === projectId;
37
+ }
38
+ catch (err) {
39
+ if (isConnRefused(err))
40
+ return null;
41
+ throw err;
42
+ }
43
+ };
44
+ /* CLI command "ase mcp" */
45
+ export default class MCPCommand {
46
+ log;
47
+ constructor(log) {
48
+ this.log = log;
49
+ }
50
+ /* load service identity context */
51
+ loadContext() {
52
+ const cfg = new Config("config", configSchema, this.log);
53
+ cfg.read();
54
+ const svc = new Config("service", serviceSchema, this.log);
55
+ svc.read();
56
+ const rawId = cfg.get("project.id");
57
+ const projectId = rawId ?? path.basename(process.cwd());
58
+ const rawPort = svc.get("port");
59
+ const port = rawPort ?? null;
60
+ return { projectId, port, svc };
61
+ }
62
+ /* spawn "ase service start" detached and wait for it to come up */
63
+ async ensureService() {
64
+ let ctx = this.loadContext();
65
+ /* fast path: already running */
66
+ if (ctx.port !== null) {
67
+ const match = await probe(ctx.port, ctx.projectId);
68
+ if (match === true)
69
+ return { projectId: ctx.projectId, port: ctx.port };
70
+ }
71
+ /* spawn "ase service start" using the same node entry point */
72
+ const entry = fileURLToPath(new URL("./ase.js", import.meta.url));
73
+ await execa(process.execPath, [entry, "service", "start"], {
74
+ stdio: "ignore",
75
+ detached: false
76
+ });
77
+ /* re-load context to pick up the freshly persisted port */
78
+ ctx = this.loadContext();
79
+ if (ctx.port === null)
80
+ throw new Error("mcp: service did not register a port after start");
81
+ const match = await probe(ctx.port, ctx.projectId);
82
+ if (match !== true)
83
+ throw new Error(`mcp: service not responding on port ${ctx.port} after start`);
84
+ return { projectId: ctx.projectId, port: ctx.port };
85
+ }
86
+ /* bridge stdio to a Streamable HTTP MCP endpoint on the local service */
87
+ async runBridge() {
88
+ /* ensure the service is running */
89
+ const { port } = await this.ensureService();
90
+ /* create MCP HTTP client */
91
+ const url = new URL(`http://${HOST}:${port}/mcp`);
92
+ const client = new StreamableHTTPClientTransport(url);
93
+ /* create MCP STDIO server */
94
+ const server = new StdioServerTransport();
95
+ /* handle shutdown */
96
+ let closed = false;
97
+ const shutdown = async () => {
98
+ if (closed)
99
+ return;
100
+ closed = true;
101
+ await Promise.allSettled([
102
+ server.close(),
103
+ client.close()
104
+ ]);
105
+ };
106
+ /* connect server to client (forward transport) */
107
+ server.onmessage = (msg) => {
108
+ client.send(msg).catch((_err) => {
109
+ const err = _err instanceof Error ? _err : new Error(String(_err));
110
+ this.log.write("error", `mcp: http send: ${err.message}`);
111
+ });
112
+ };
113
+ server.onerror = (_err) => {
114
+ const err = _err instanceof Error ? _err : new Error(String(_err));
115
+ this.log.write("error", `mcp: stdio: ${err.message}`);
116
+ };
117
+ server.onclose = () => {
118
+ shutdown().catch(() => { });
119
+ };
120
+ /* connect client to server (backward transport) */
121
+ client.onmessage = (msg) => {
122
+ server.send(msg).catch((_err) => {
123
+ const err = _err instanceof Error ? _err : new Error(String(_err));
124
+ this.log.write("error", `mcp: stdout send: ${err.message}`);
125
+ });
126
+ };
127
+ client.onerror = (_err) => {
128
+ const err = _err instanceof Error ? _err : new Error(String(_err));
129
+ this.log.write("error", `mcp: http: ${err.message}`);
130
+ };
131
+ client.onclose = () => {
132
+ shutdown().catch(() => { });
133
+ };
134
+ /* start server and client */
135
+ await server.start();
136
+ await client.start();
137
+ /* await stdio to be closed */
138
+ await new Promise((resolve) => {
139
+ const done = () => resolve();
140
+ process.stdin.once("end", done);
141
+ process.stdin.once("close", done);
142
+ });
143
+ /* shutdown services */
144
+ await shutdown();
145
+ return 0;
146
+ }
147
+ /* register commands */
148
+ register(program) {
149
+ program
150
+ .command("mcp")
151
+ .description("Bridge stdio MCP to the per-project background service over Streamable HTTP")
152
+ .action(async () => {
153
+ process.exit(await this.runBridge());
154
+ });
155
+ }
156
+ }
@@ -13,7 +13,12 @@ import axios from "axios";
13
13
  import { isMap } from "yaml";
14
14
  import * as v from "valibot";
15
15
  import prettyMs from "pretty-ms";
16
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
18
+ import { z } from "zod";
16
19
  import { Config, configSchema } from "./ase-config.js";
20
+ import { renderDiagram } from "./ase-diagram.js";
21
+ import pkg from "../package.json" with { type: "json" };
17
22
  const SERVE_ENV = "ASE_SERVICE_SERVE";
18
23
  const PORT_ENV = "ASE_SERVICE_PORT";
19
24
  const HOST = "127.0.0.1";
@@ -177,6 +182,86 @@ export default class ServiceCommand {
177
182
  lastActivity = Date.now();
178
183
  return h.continue;
179
184
  });
185
+ /* build a fresh MCP server instance with the demo "ping" tool */
186
+ const buildMcpServer = () => {
187
+ const mcp = new McpServer({ name: "ase", version: pkg.version });
188
+ mcp.registerTool("ping", {
189
+ title: "ASE service ping",
190
+ description: "Return ASE service identity, port, and uptime.",
191
+ inputSchema: {}
192
+ }, async () => {
193
+ const status = {
194
+ ok: true,
195
+ projectId: ctx.projectId,
196
+ port: ctx.port,
197
+ uptimeMs: Date.now() - startTime
198
+ };
199
+ return {
200
+ content: [{ type: "text", text: JSON.stringify(status) }]
201
+ };
202
+ });
203
+ mcp.registerTool("diagram", {
204
+ title: "ASE diagram render",
205
+ description: "Render a Mermaid diagram as Unicode/ASCII art. " +
206
+ "Use for visualizing " +
207
+ "structure/layout/components/dependencies as a Flowchart, " +
208
+ "control-flow/branching/concurrency as a Flowchart, " +
209
+ "state-machine/states/transitions as an UML State Diagram, " +
210
+ "data-flow/actors/messages/protocols as an UML Sequence Diagram, " +
211
+ "data-structure/classes/methods as an UML Class Diagram " +
212
+ "data-model/entities/relationships as an ER Diagram, or " +
213
+ "metrics/distributions/time-series as a XY-Charts. " +
214
+ "Pass the Mermaid diagram specification as `diagram`. " +
215
+ "Returns the rendered art as `text`.",
216
+ inputSchema: {
217
+ diagram: z.string()
218
+ .describe("Mermaid diagram specification"),
219
+ ascii: z.boolean().default(false)
220
+ .describe("emit plain ASCII (+-|) instead of Unicode box-drawing characters"),
221
+ colorMode: z.enum(["none", "ansi16", "ansi256"]).default("none")
222
+ .describe("color mode for ANSI escape sequences in the rendered output"),
223
+ nodeMarginX: z.number().int().min(0).default(3)
224
+ .describe("horizontal margin between nodes, in characters"),
225
+ nodeMarginY: z.number().int().min(0).default(3)
226
+ .describe("vertical margin between nodes, in lines"),
227
+ nodePadding: z.number().int().min(0).default(1)
228
+ .describe("inner horizontal and vertical padding within each node, in characters"),
229
+ diagramClipX: z.number().int().min(0).default(0)
230
+ .describe("extra horizontal clipping: subtract this many characters from `terminalWidth`"),
231
+ diagramClipY: z.number().int().min(0).default(0)
232
+ .describe("extra vertical clipping: subtract this many lines from `terminalHeight`"),
233
+ terminalWidth: z.number().int().min(0).default(0)
234
+ .describe("terminal width in characters; 0 disables horizontal clipping"),
235
+ terminalHeight: z.number().int().min(0).default(0)
236
+ .describe("terminal height in lines; 0 disables vertical clipping")
237
+ }
238
+ }, async (args) => {
239
+ try {
240
+ const out = renderDiagram(args.diagram, {
241
+ ascii: args.ascii,
242
+ colorMode: args.colorMode,
243
+ nodeMarginX: args.nodeMarginX,
244
+ nodeMarginY: args.nodeMarginY,
245
+ nodePadding: args.nodePadding,
246
+ diagramClipX: args.diagramClipX,
247
+ diagramClipY: args.diagramClipY,
248
+ terminalWidth: args.terminalWidth,
249
+ terminalHeight: args.terminalHeight
250
+ });
251
+ return {
252
+ content: [{ type: "text", text: out }]
253
+ };
254
+ }
255
+ catch (err) {
256
+ const message = err instanceof Error ? err.message : String(err);
257
+ return {
258
+ isError: true,
259
+ content: [{ type: "text", text: `diagram: render failed: ${message}` }]
260
+ };
261
+ }
262
+ });
263
+ return mcp;
264
+ };
180
265
  /* listen to HTTP/REST endpoints */
181
266
  server.route({
182
267
  method: "OPTIONS",
@@ -196,6 +281,38 @@ export default class ServiceCommand {
196
281
  return h.response({ ok: true }).code(200);
197
282
  }
198
283
  });
284
+ const mcpHandler = async (request, h, body) => {
285
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
286
+ const mcp = buildMcpServer();
287
+ request.raw.res.on("close", () => {
288
+ transport.close().catch(() => { });
289
+ mcp.close().catch(() => { });
290
+ });
291
+ try {
292
+ await mcp.connect(transport);
293
+ await transport.handleRequest(request.raw.req, request.raw.res, body);
294
+ }
295
+ catch (_err) {
296
+ const err = _err instanceof Error ? _err : new Error(String(_err));
297
+ this.log.write("error", `mcp: ${err.message}`);
298
+ if (!request.raw.res.headersSent) {
299
+ request.raw.res.statusCode = 500;
300
+ request.raw.res.end();
301
+ }
302
+ }
303
+ return h.abandon;
304
+ };
305
+ server.route({
306
+ method: "POST",
307
+ path: "/mcp",
308
+ options: { payload: { parse: true, allow: "application/json" } },
309
+ handler: (request, h) => mcpHandler(request, h, request.payload)
310
+ });
311
+ server.route({
312
+ method: ["GET", "DELETE"],
313
+ path: "/mcp",
314
+ handler: (request, h) => mcpHandler(request, h)
315
+ });
199
316
  server.route({
200
317
  method: "POST",
201
318
  path: "/command",
package/dst/ase.js CHANGED
@@ -8,7 +8,9 @@ import { Command, CommanderError, Option } from "commander";
8
8
  import Log from "./ase-log.js";
9
9
  import ConfigCommand from "./ase-config.js";
10
10
  import ServiceCommand from "./ase-service.js";
11
+ import MCPCommand from "./ase-mcp.js";
11
12
  import HookCommand from "./ase-hook.js";
13
+ import DiagramCommand from "./ase-diagram.js";
12
14
  import pkg from "../package.json" with { type: "json" };
13
15
  /* globally initialize logger */
14
16
  const log = new Log("ase", "warning", "-");
@@ -39,7 +41,9 @@ const main = async () => {
39
41
  /* register top-level commands */
40
42
  new ConfigCommand(log).register(program);
41
43
  new ServiceCommand(log).register(program);
44
+ new MCPCommand(log).register(program);
42
45
  new HookCommand(log).register(program);
46
+ new DiagramCommand(log).register(program);
43
47
  /* parse program arguments */
44
48
  await program.parseAsync(process.argv);
45
49
  /* gracefully terminate */
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "homepage": "http://github.com/rse/ase",
7
7
  "repository": { "url": "git+https://github.com/rse/ase.git", "type": "git" },
8
8
  "bugs": { "url": "http://github.com/rse/ase/issues" },
9
- "version": "0.0.13",
9
+ "version": "0.0.15",
10
10
  "license": "GPL-3.0-only",
11
11
  "author": {
12
12
  "name": "Dr. Ralf S. Engelschall",
@@ -18,13 +18,13 @@
18
18
  "devDependencies": {
19
19
  "eslint": "9.39.4",
20
20
  "@eslint/js": "9.39.4",
21
- "@typescript-eslint/parser": "8.58.2",
22
- "@typescript-eslint/eslint-plugin": "8.58.2",
21
+ "@typescript-eslint/parser": "8.59.1",
22
+ "@typescript-eslint/eslint-plugin": "8.59.1",
23
23
  "eslint-plugin-n": "17.24.0",
24
- "eslint-plugin-promise": "7.2.1",
24
+ "eslint-plugin-promise": "7.3.0",
25
25
  "eslint-plugin-import": "2.32.0",
26
26
  "neostandard": "0.13.0",
27
- "globals": "17.5.0",
27
+ "globals": "17.6.0",
28
28
  "typescript": "6.0.3",
29
29
 
30
30
  "@rse/stx": "1.1.5",
@@ -41,11 +41,14 @@
41
41
  "execa": "9.6.1",
42
42
  "mkdirp": "3.0.1",
43
43
  "@hapi/hapi": "21.4.8",
44
- "axios": "1.15.1",
44
+ "axios": "1.15.2",
45
+ "beautiful-mermaid": "1.1.3",
45
46
  "cli-table3": "0.6.5",
46
47
  "chalk": "5.6.2",
47
48
  "pretty-ms": "9.3.0",
48
- "luxon": "3.7.2"
49
+ "luxon": "3.7.2",
50
+ "@modelcontextprotocol/sdk": "1.29.0",
51
+ "zod": "4.4.2"
49
52
  },
50
53
  "engines": {
51
54
  "npm": ">=10.0.0",