@velajs/cli 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -19,8 +19,29 @@ pnpm add -D @velajs/cli
19
19
  | `vela module graph` | Module graph: imports tree with `global`/`lazy` flags and provider counts (`--json` for the raw graph). |
20
20
  | `vela entrypoint list` | Declared entrypoint kinds (websocket, queue, cron, …) and their entries — lazy modules stay unmaterialized. |
21
21
  | `vela openapi dump` | Emit the OpenAPI document (needs `rootModule` in the config; `--out`, `--title`, `--api-version`, `--global-prefix`). |
22
+ | `vela mcp serve` | Run a Model Context Protocol stdio server exposing the introspection above as read-only tools (`route_list`, `module_graph`, `entrypoint_list`, `openapi_dump`, `token_describe`) plus a `vela://openapi` resource — for AI agents. |
22
23
 
23
- All introspection commands take `--config <path>` and `--json`.
24
+ All introspection commands take `--config <path>`; the four listing/dump commands also take `--json`.
25
+
26
+ ### MCP server
27
+
28
+ `vela mcp serve` builds the app from `vela.config` and speaks the [Model Context
29
+ Protocol](https://modelcontextprotocol.io) over stdio, so an AI agent can query
30
+ the app's shape. It exposes read-only tools — `route_list`, `module_graph`
31
+ (`{ tree? }`), `entrypoint_list`, `openapi_dump` (`{ globalPrefix?, title?,
32
+ apiVersion? }`, needs `rootModule`), `token_describe` (`{ token }`) — and, when
33
+ `rootModule` is set, a `vela://openapi` resource. Tool results are JSON text.
34
+ stdout carries only JSON-RPC; all logging goes to stderr. The server runs until
35
+ the client disconnects, then disposes the app.
36
+
37
+ ```jsonc
38
+ // e.g. in an MCP client config
39
+ {
40
+ "mcpServers": {
41
+ "vela": { "command": "vela", "args": ["mcp", "serve"] }
42
+ }
43
+ }
44
+ ```
24
45
 
25
46
  ## Configure
26
47
 
@@ -0,0 +1,17 @@
1
+ import { Command } from 'clipanion';
2
+ /**
3
+ * `vela mcp serve` — an MCP stdio server exposing the same READ-ONLY
4
+ * introspection as the `route`/`module`/`entrypoint`/`openapi` commands, so an
5
+ * AI agent can query a Vela app's shape over the Model Context Protocol.
6
+ *
7
+ * Deliberately does NOT extend `AppCommand`: that base disposes the app in its
8
+ * `finally` the moment `run()` returns, but an MCP server must stay alive until
9
+ * the transport closes. stdout is reserved for JSON-RPC framing; every human
10
+ * message goes to stderr.
11
+ */
12
+ export declare class McpServeCommand extends Command {
13
+ static paths: string[][];
14
+ static usage: import("clipanion").Usage;
15
+ config: string | undefined;
16
+ execute(): Promise<number>;
17
+ }
@@ -0,0 +1,166 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { createOpenApiDocument } from '@velajs/vela';
5
+ import { Command, Option } from 'clipanion';
6
+ import { z } from 'zod';
7
+ import { loadConfig } from '../config.js';
8
+ import { collectEntrypoints, collectModules, collectRoutes, renderModuleTree } from '../introspect.js';
9
+ const OPENAPI_URI = 'vela://openapi';
10
+ /** A single JSON text block — the shape every tool/resource result uses. */
11
+ function jsonText(data) {
12
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
13
+ }
14
+ /** An MCP tool error result (JSON-RPC stays 2.0; the failure is in-band). */
15
+ function toolError(message) {
16
+ return { isError: true, content: [{ type: 'text', text: message }] };
17
+ }
18
+ /** Name + version for the MCP server handshake, read from the CLI's own package.json. */
19
+ async function readCliIdentity() {
20
+ const here = dirname(fileURLToPath(import.meta.url));
21
+ const pkgPath = join(here, '..', '..', 'package.json');
22
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
23
+ return { name: pkg.name ?? '@velajs/cli', version: pkg.version ?? '0.0.0' };
24
+ }
25
+ /**
26
+ * String-label lookup for a DI token across the module graph. Reads only the
27
+ * serializable descriptions (`collectModules`) — never resolves the token or
28
+ * constructs anything. Reports which modules provide/export it and their scope
29
+ * flags, plus whether the string names a module itself.
30
+ */
31
+ function describeToken(app, token) {
32
+ const modules = collectModules(app);
33
+ const providedBy = modules
34
+ .filter((m) => m.providers.includes(token))
35
+ .map((m) => ({ moduleId: m.moduleId, isGlobal: m.isGlobal, lazy: m.lazy, exported: m.exports.includes(token) }));
36
+ const matchesModule = modules.find((m) => m.moduleId === token);
37
+ return {
38
+ token,
39
+ found: providedBy.length > 0 || matchesModule !== undefined,
40
+ providedBy,
41
+ module: matchesModule
42
+ ? { moduleId: matchesModule.moduleId, isGlobal: matchesModule.isGlobal, lazy: matchesModule.lazy }
43
+ : null,
44
+ };
45
+ }
46
+ /**
47
+ * `vela mcp serve` — an MCP stdio server exposing the same READ-ONLY
48
+ * introspection as the `route`/`module`/`entrypoint`/`openapi` commands, so an
49
+ * AI agent can query a Vela app's shape over the Model Context Protocol.
50
+ *
51
+ * Deliberately does NOT extend `AppCommand`: that base disposes the app in its
52
+ * `finally` the moment `run()` returns, but an MCP server must stay alive until
53
+ * the transport closes. stdout is reserved for JSON-RPC framing; every human
54
+ * message goes to stderr.
55
+ */
56
+ export class McpServeCommand extends Command {
57
+ static paths = [['mcp', 'serve']];
58
+ static usage = Command.Usage({
59
+ category: 'Introspection',
60
+ description: 'Serve Vela introspection as MCP tools over stdio (for AI agents).',
61
+ details: 'Builds the app from vela.config and runs a Model Context Protocol stdio server. Exposes ' +
62
+ 'read-only tools (route_list, module_graph, entrypoint_list, openapi_dump, token_describe) ' +
63
+ 'and — when the config declares a rootModule — a `vela://openapi` resource. stdout carries ' +
64
+ 'only JSON-RPC; all logging goes to stderr. The server runs until the client disconnects.',
65
+ examples: [
66
+ ['Serve over stdio', 'vela mcp serve'],
67
+ ['Use a specific config', 'vela mcp serve --config ./config/vela.config.js'],
68
+ ],
69
+ });
70
+ config = Option.String('--config', { description: 'Path to the vela config file.' });
71
+ async execute() {
72
+ const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
73
+ const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
74
+ const log = (message) => {
75
+ this.context.stderr.write(`${message}\n`);
76
+ };
77
+ const velaConfig = await loadConfig(process.cwd(), this.config);
78
+ const app = await velaConfig.createApp();
79
+ const rootModule = velaConfig.rootModule;
80
+ try {
81
+ const identity = await readCliIdentity();
82
+ const server = new McpServer(identity);
83
+ server.registerTool('route_list', {
84
+ description: 'The app\'s HTTP route table: framework-composed controller routes (method, full ' +
85
+ 'path, Controller#handler) plus everything else mounted on the router, labeled ' +
86
+ '(mounted). Empty when the app builds no HTTP routes.',
87
+ inputSchema: {},
88
+ }, () => jsonText(collectRoutes(app) ?? []));
89
+ server.registerTool('module_graph', {
90
+ description: 'The loaded module graph as serializable descriptions (providers, exports, imports, ' +
91
+ 'global/lazy flags). Pass tree=true to also get the rendered import tree lines.',
92
+ inputSchema: { tree: z.boolean().optional() },
93
+ }, ({ tree }) => {
94
+ const modules = collectModules(app);
95
+ return jsonText(tree ? { modules, tree: renderModuleTree(modules) } : modules);
96
+ });
97
+ server.registerTool('entrypoint_list', {
98
+ description: 'Every declared entrypoint kind (websocket, queue, cron, …) with its entries and ' +
99
+ 'metadata — including kinds with zero entries. Lazy modules stay unmaterialized.',
100
+ inputSchema: {},
101
+ }, () => jsonText(collectEntrypoints(app)));
102
+ server.registerTool('openapi_dump', {
103
+ description: 'The OpenAPI 3.1 document for the app. Requires a rootModule in vela.config. ' +
104
+ 'globalPrefix/title/apiVersion override the defaults (the app global prefix and ' +
105
+ 'the module-derived info).',
106
+ inputSchema: {
107
+ globalPrefix: z.string().optional(),
108
+ title: z.string().optional(),
109
+ apiVersion: z.string().optional(),
110
+ },
111
+ }, ({ globalPrefix, title, apiVersion }) => {
112
+ if (!rootModule) {
113
+ return toolError('openapi_dump needs the root module. Add `rootModule: AppModule` to your vela.config.');
114
+ }
115
+ const info = {};
116
+ if (title)
117
+ info.title = title;
118
+ if (apiVersion)
119
+ info.version = apiVersion;
120
+ const document = createOpenApiDocument(rootModule, {
121
+ globalPrefix: globalPrefix ?? app.getGlobalPrefix(),
122
+ ...(Object.keys(info).length > 0 ? { info } : {}),
123
+ });
124
+ return jsonText(document);
125
+ });
126
+ server.registerTool('token_describe', {
127
+ description: 'Look a DI token STRING LABEL up across the module graph: which modules provide/export ' +
128
+ 'it and their scope flags, plus whether the string names a module. Read-only string ' +
129
+ 'match — does not resolve or construct the token.',
130
+ inputSchema: { token: z.string() },
131
+ }, ({ token }) => jsonText(describeToken(app, token)));
132
+ if (rootModule) {
133
+ server.registerResource('openapi', OPENAPI_URI, { description: 'The OpenAPI 3.1 document for the app.', mimeType: 'application/json' }, () => ({
134
+ contents: [
135
+ {
136
+ uri: OPENAPI_URI,
137
+ mimeType: 'application/json',
138
+ text: JSON.stringify(createOpenApiDocument(rootModule, { globalPrefix: app.getGlobalPrefix() }), null, 2),
139
+ },
140
+ ],
141
+ }));
142
+ }
143
+ const transport = new StdioServerTransport();
144
+ const closed = new Promise((resolvePromise) => {
145
+ transport.onclose = resolvePromise;
146
+ });
147
+ await server.connect(transport);
148
+ log(`vela mcp serve — ready (5 tools${rootModule ? ' + vela://openapi resource' : ''}). ` +
149
+ 'Awaiting client on stdio; stdout is JSON-RPC only.');
150
+ // Keep the process alive until the client disconnects; only then dispose.
151
+ await closed;
152
+ return 0;
153
+ }
154
+ finally {
155
+ const dispose = app.dispose;
156
+ if (typeof dispose === 'function') {
157
+ try {
158
+ await dispose.call(app);
159
+ }
160
+ catch (error) {
161
+ this.context.stderr.write(`Warning: teardown failed: ${String(error)}\n`);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  export { SeedCommand } from './commands/seed.command.js';
3
3
  export { EntrypointListCommand, ModuleGraphCommand, OpenApiDumpCommand, RouteListCommand, } from './commands/introspect.commands.js';
4
+ export { McpServeCommand } from './commands/mcp.command.js';
4
5
  export { collectRoutes, collectModules, collectEntrypoints, renderModuleTree } from './introspect.js';
5
6
  export type { RouteRow, EntrypointRow } from './introspect.js';
6
7
  export { renderTable } from './format.js';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Builtins, Cli } from 'clipanion';
3
3
  import { EntrypointListCommand, ModuleGraphCommand, OpenApiDumpCommand, RouteListCommand, } from './commands/introspect.commands.js';
4
+ import { McpServeCommand } from './commands/mcp.command.js';
4
5
  import { SeedCommand } from './commands/seed.command.js';
5
6
  const cli = new Cli({
6
7
  binaryName: 'vela',
@@ -14,9 +15,11 @@ cli.register(RouteListCommand);
14
15
  cli.register(ModuleGraphCommand);
15
16
  cli.register(EntrypointListCommand);
16
17
  cli.register(OpenApiDumpCommand);
18
+ cli.register(McpServeCommand);
17
19
  void cli.runExit(process.argv.slice(2));
18
20
  export { SeedCommand } from './commands/seed.command.js';
19
21
  export { EntrypointListCommand, ModuleGraphCommand, OpenApiDumpCommand, RouteListCommand, } from './commands/introspect.commands.js';
22
+ export { McpServeCommand } from './commands/mcp.command.js';
20
23
  export { collectRoutes, collectModules, collectEntrypoints, renderModuleTree } from './introspect.js';
21
24
  export { renderTable } from './format.js';
22
25
  export { loadConfig, defineVelaConfig } from './config.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "CLI for Vela apps — seeding and project tasks (Node-side; not bundled into the edge Worker)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,9 @@
26
26
  "node": ">=20"
27
27
  },
28
28
  "dependencies": {
29
- "clipanion": "^4.0.0-rc.4"
29
+ "@modelcontextprotocol/sdk": "^1.29.0",
30
+ "clipanion": "^4.0.0-rc.4",
31
+ "zod": "^3.25.76"
30
32
  },
31
33
  "peerDependencies": {
32
34
  "@velajs/vela": ">=1.15.0"
@@ -47,6 +49,10 @@
47
49
  "cloudflare-workers"
48
50
  ],
49
51
  "license": "MIT",
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/velajs/cli.git"
55
+ },
50
56
  "scripts": {
51
57
  "build": "rm -rf dist && tsc",
52
58
  "typecheck": "tsc --noEmit",