@stone-js/mcp-dev 0.8.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.
package/dist/index.js ADDED
@@ -0,0 +1,786 @@
1
+ import { join } from 'node:path';
2
+ import { writeFileSync, readFileSync, existsSync } from 'node:fs';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { RuntimeError, classDecoratorLegacyWrapper, addBlueprint } from '@stone-js/core';
6
+
7
+ /* v8 ignore start -- thin filesystem defaults */
8
+ const defaultIo = {
9
+ exists: (path) => existsSync(path),
10
+ read: (path) => readFileSync(path, 'utf-8'),
11
+ write: (path, content) => writeFileSync(path, content, 'utf-8')
12
+ };
13
+ /* v8 ignore stop */
14
+ /**
15
+ * The `.mcp.json` server entry that launches this dev server.
16
+ *
17
+ * @param command - The launcher command (defaults to `stone`).
18
+ * @returns The MCP server entry.
19
+ */
20
+ function mcpServerEntry(command = 'stone') {
21
+ return { command, args: ['mcp'] };
22
+ }
23
+ /**
24
+ * Merge the `stone` server into an existing `.mcp.json` object without clobbering anything.
25
+ *
26
+ * It only adds the `stone` entry when absent, so a developer's own config (other servers, or a
27
+ * customized `stone` entry) is preserved.
28
+ *
29
+ * @param existing - The parsed `.mcp.json` (or undefined when the file does not exist).
30
+ * @returns The merged config and whether it changed.
31
+ */
32
+ function mergeMcpJson(existing) {
33
+ const config = { ...(existing ?? {}) };
34
+ const servers = { ...(config.mcpServers ?? {}) };
35
+ const changed = servers.stone === undefined;
36
+ if (changed) {
37
+ servers.stone = mcpServerEntry();
38
+ }
39
+ config.mcpServers = servers;
40
+ return { config, changed };
41
+ }
42
+ /**
43
+ * Create or update `.mcp.json` at `cwd` so a coding agent discovers this server. Idempotent: it
44
+ * writes only when the `stone` entry is missing, and never overwrites the rest of the file.
45
+ *
46
+ * @param cwd - The project root.
47
+ * @param io - The filesystem surface (defaults to `node:fs`).
48
+ * @returns The file path and whether it was written.
49
+ */
50
+ function initMcpJson(cwd, io = defaultIo) {
51
+ const file = join(cwd, '.mcp.json');
52
+ let existing;
53
+ if (io.exists(file)) {
54
+ try {
55
+ existing = JSON.parse(io.read(file));
56
+ }
57
+ catch {
58
+ existing = undefined;
59
+ }
60
+ }
61
+ const { config, changed } = mergeMcpJson(existing);
62
+ if (changed) {
63
+ io.write(file, `${JSON.stringify(config, null, 2)}\n`);
64
+ }
65
+ return { file, changed };
66
+ }
67
+ /**
68
+ * Whether a `.mcp.json` exists at `cwd`.
69
+ *
70
+ * @param cwd - The project root.
71
+ * @param io - The filesystem surface (defaults to `node:fs`).
72
+ * @returns True when the file exists.
73
+ */
74
+ function hasMcpJson(cwd, io = defaultIo) {
75
+ return io.exists(join(cwd, '.mcp.json'));
76
+ }
77
+
78
+ const concepts = [
79
+ { id: 'continuum', title: 'Continuum Architecture', summary: 'An application is not an object but an act: Application = Domain × Context → Resolution. Stone.js IS the context: you write your domain once and the context applies to it at runtime. Focus on your domain during development; choose where to deploy at the end.' },
80
+ { id: 'domain-vs-context', title: 'Domain vs Context', summary: 'The domain is your business logic (handlers, services). The context is the execution environment (HTTP, CLI, browser, edge, MCP). Stone.js owns the context so a single domain runs in any of them — backend, frontend, and mobile later.' },
81
+ { id: 'blueprint', title: 'Blueprint (Setup)', summary: 'A single configuration manifest built once before any event, by introspecting decorators or via imperative meta-modules. All configuration lives under dotted `stone.*` keys.' },
82
+ { id: 'kernel', title: 'Kernel (Initialization)', summary: 'Applies the container (an ephemeral per-event execution context) and the domain to the intention; runs middleware, hooks and error handlers. The micro-kernel depends only on pipeline, service-container and config.' },
83
+ { id: 'adapter', title: 'Adapter (Integration)', summary: 'One package per platform. Captures raw causes, normalises them into intentions (IncomingEvent), and turns responses back into native effects. The adapter is the only long-lived, shared, launched-once thing.' },
84
+ { id: 'ephemeral-context', title: 'Ephemeral per-request context', summary: 'Each request creates a fresh container (a new ephemeral context) via the kernel — totally isolated, never shared between requests. App-lifetime state belongs in a shared scope (a module-level singleton, a cache, or the adapter), not the container.' },
85
+ { id: 'two-paradigms', title: 'Two paradigms at parity', summary: 'Declarative (TC39 stage-3 decorators, Symbol.metadata) and imperative (define* helpers → meta-modules). Both are first-class and 1:1. Three forms everywhere: class, factory, function (the function form never receives the container).' },
86
+ { id: 'service-container', title: 'Service container (DI)', summary: 'A Proxy-based container whose `get` auto-wires dependencies from a destructured constructor (`constructor ({ logger, telemetry })`). Register services as class, factory (singleton optional), never the function form for providers.' },
87
+ { id: 'service-provider', title: 'Service provider', summary: 'Registers services/bindings into the container during kernel init. Modules contribute providers via their blueprint; nothing depends back on the core (micro-kernel).' },
88
+ { id: 'middleware', title: 'Middleware & hooks', summary: 'A chain-of-responsibility pipeline wraps event handling (global > local priority). Lifecycle hooks (onInit, onEvent, onTerminate, …) let modules observe the flow without coupling.' }
89
+ ];
90
+ const modules = [
91
+ { package: '@stone-js/pipeline', summary: 'Chain-of-responsibility primitive.', tier: 'primitive' },
92
+ { package: '@stone-js/service-container', summary: 'Proxy-based dependency-injection container.', tier: 'primitive' },
93
+ { package: '@stone-js/config', summary: 'Dotted-key blueprint store.', tier: 'primitive' },
94
+ { package: '@stone-js/core', summary: 'The micro-kernel: blueprint, kernel, adapter base, lifecycle.', tier: 'core' },
95
+ { package: '@stone-js/http-core', summary: 'Runtime-agnostic HTTP primitives (events, responses, cookies).', tier: 'crosscutting' },
96
+ { package: '@stone-js/router', summary: 'Universal router (node & browser).', tier: 'crosscutting' },
97
+ { package: '@stone-js/env', summary: 'Environment access with masking.', tier: 'crosscutting' },
98
+ { package: '@stone-js/filesystem', summary: 'Filesystem + file abstractions.', tier: 'crosscutting' },
99
+ { package: '@stone-js/browser-core', summary: 'Browser-side primitives.', tier: 'crosscutting' },
100
+ { package: '@stone-js/node-http-adapter', summary: 'Node HTTP server adapter.', tier: 'adapter' },
101
+ { package: '@stone-js/node-cli-adapter', summary: 'Node CLI adapter.', tier: 'adapter' },
102
+ { package: '@stone-js/aws-lambda-adapter', summary: 'Generic AWS Lambda adapter.', tier: 'adapter' },
103
+ { package: '@stone-js/aws-lambda-http-adapter', summary: 'AWS Lambda HTTP (API GW v1/v2, ALB) adapter.', tier: 'adapter' },
104
+ { package: '@stone-js/browser-adapter', summary: 'Browser SPA adapter.', tier: 'adapter' },
105
+ { package: '@stone-js/fetch-adapter', summary: 'Web-standard (WinterCG) adapter: one build → Cloudflare/Deno/Bun/Vercel/Netlify edge.', tier: 'adapter' },
106
+ { package: '@stone-js/use-react', summary: 'React view engine (CSR/SSR/SSG).', tier: 'frontend' },
107
+ { package: '@stone-js/use-view', summary: 'Agnostic view-engine layer.', tier: 'frontend' },
108
+ { package: '@stone-js/telemetry', summary: 'Spans/counters/gauges via hooks + middleware, pluggable exporters.', tier: 'extension' },
109
+ { package: '@stone-js/validation', summary: 'One schema (Zod/Standard Schema) validated backend AND frontend.', tier: 'extension' },
110
+ { package: '@stone-js/auth', summary: 'Edge-native, stateless JWT/OAuth (jose).', tier: 'extension' },
111
+ { package: '@stone-js/authz', summary: 'Isomorphic RBAC+ABAC authorization (CASL).', tier: 'extension' },
112
+ { package: '@stone-js/resources', summary: 'API resources: shape the exposed output, decoupled from controllers.', tier: 'extension' },
113
+ { package: '@stone-js/openapi', summary: 'Derive an OpenAPI contract from Zod schemas + routes.', tier: 'extension' },
114
+ { package: '@stone-js/testing', summary: 'Boot an app in-memory and dispatch events through the kernel.', tier: 'extension' },
115
+ { package: '@stone-js/mcp-dev', summary: 'Serve the framework knowledge + your tools to a coding agent via `stone mcp` (MCP, stdio).', tier: 'tooling' },
116
+ { package: '@stone-js/cli', summary: 'Build tooling (Rollup+Babel backend, Vite+Babel frontend, codegen).', tier: 'tooling' },
117
+ { package: '@stone-js/create', summary: 'Scaffolder: npm create @stone-js.', tier: 'tooling' }
118
+ ];
119
+ const bestPractices = [
120
+ { rule: 'Keep every module core platform-agnostic (no window/process/fs); add platform drivers/adapters around it.', why: 'A single domain must run backend, frontend and (later) mobile.' },
121
+ { rule: 'Use TC39 stage-3 decorators (Symbol.metadata). Never enable experimentalDecorators or reflect-metadata.', why: 'Setting experimentalDecorators flips esbuild/tsc to legacy decorators and breaks method decorators; the CLI builds with Babel stage-3.' },
122
+ { rule: 'Configure everything via dotted stone.* keys on the blueprint.', why: 'One uniform, introspectable configuration surface.' },
123
+ { rule: 'Expose class, factory and function forms; the function form never receives the container.', why: 'Two paradigms at parity, DI only where it makes sense.' },
124
+ { rule: 'Private/protected constructor + static create().', why: 'Controlled construction across the framework.' },
125
+ { rule: 'Declare internal @stone-js/* deps as workspace:* in the monorepo.', why: 'Avoids resolving stale published versions (a real source of breakage).' },
126
+ { rule: 'Put app-lifetime state in a shared scope (module-level singleton, cache, adapter), never in the per-request container.', why: 'The container is a fresh ephemeral context per request — it cannot and must not persist across requests.' },
127
+ { rule: 'Every bug fix earns a behavioural test (not a mock test); target 100% coverage.', why: 'Prove behaviour, not implementation.' },
128
+ { rule: 'Attach request-scoped state via setMetadataValue/getMetadataValue; the principal via setUserResolver.', why: 'The idiomatic per-event carriers.' }
129
+ ];
130
+ const gaps = [
131
+ { name: 'queue/jobs', status: 'planned', note: 'Background jobs (in-memory, Redis/BullMQ, SQS, Cloud Tasks).' },
132
+ { name: 'cache', status: 'planned', note: 'Agnostic cache (memory, Redis, CF KV) — the legitimate shared scope.' },
133
+ { name: 'mail/notifications', status: 'planned', note: 'Multi-channel notifications.' },
134
+ { name: 'rate-limiting', status: 'planned', note: 'Edge-friendly throttling.' },
135
+ { name: 'i18n', status: 'planned', note: 'Runtime localization.' },
136
+ { name: 'websocket/realtime', status: 'planned', note: 'Channels/rooms/presence, agnostic drivers.' },
137
+ { name: 'cloud file drivers', status: 'planned', note: 'S3/R2/GCS drivers extending @stone-js/filesystem.' },
138
+ { name: 'ORM', status: 'missing', note: 'By design: integrate Drizzle/Prisma/Kysely via providers — Stone.js will not ship an ORM.' }
139
+ ];
140
+ /**
141
+ * The single, curated, machine-readable map of Stone.js. Kept concise and accurate so an agent
142
+ * can consult it in real time instead of scanning every package.
143
+ */
144
+ const knowledgeBase = {
145
+ name: 'Stone.js',
146
+ tagline: 'Focus on your domain. Stone.js is the context. Build once, deploy anywhere.',
147
+ version: '0.8.0',
148
+ concepts,
149
+ modules,
150
+ bestPractices,
151
+ gaps
152
+ };
153
+ /**
154
+ * Find a concept by id (case-insensitive).
155
+ *
156
+ * @param id - The concept id.
157
+ * @returns The concept, or undefined.
158
+ */
159
+ function getConcept(id) {
160
+ return knowledgeBase.concepts.find((concept) => concept.id === id.toLowerCase());
161
+ }
162
+ /**
163
+ * Full-text search across concepts, modules, best-practices and gaps.
164
+ *
165
+ * @param query - The search terms.
166
+ * @returns Matching entries with their kind.
167
+ */
168
+ function searchKnowledge(query) {
169
+ const q = query.trim().toLowerCase();
170
+ if (q.length === 0) {
171
+ return [];
172
+ }
173
+ const results = [];
174
+ const match = (text) => text.toLowerCase().includes(q);
175
+ for (const c of knowledgeBase.concepts) {
176
+ if (match(c.id) || match(c.title) || match(c.summary)) {
177
+ results.push({ kind: 'concept', title: c.title, text: c.summary });
178
+ }
179
+ }
180
+ for (const m of knowledgeBase.modules) {
181
+ if (match(m.package) || match(m.summary)) {
182
+ results.push({ kind: 'module', title: m.package, text: m.summary });
183
+ }
184
+ }
185
+ for (const b of knowledgeBase.bestPractices) {
186
+ if (match(b.rule) || match(b.why)) {
187
+ results.push({ kind: 'best-practice', title: b.rule, text: b.why });
188
+ }
189
+ }
190
+ for (const g of knowledgeBase.gaps) {
191
+ if (match(g.name) || match(g.note)) {
192
+ results.push({ kind: 'gap', title: g.name, text: g.note });
193
+ }
194
+ }
195
+ return results;
196
+ }
197
+
198
+ /**
199
+ * Generates the concise `llms.txt` index (the emerging standard: a short, link-friendly Markdown
200
+ * map an agent can read in one shot). Serve it at `/llms.txt` from the docs site.
201
+ *
202
+ * @param base - The knowledge base (defaults to the built-in one).
203
+ * @returns The `llms.txt` content.
204
+ */
205
+ function generateLlmsTxt(base = knowledgeBase) {
206
+ const concepts = base.concepts.map((c) => `- **${c.title}**: ${c.summary}`).join('\n');
207
+ const modules = base.modules.map((m) => `- \`${m.package}\` (${m.tier}): ${m.summary}`).join('\n');
208
+ return `# ${base.name}
209
+
210
+ > ${base.tagline} (v${base.version})
211
+
212
+ ## Core concepts
213
+
214
+ ${concepts}
215
+
216
+ ## Modules
217
+
218
+ ${modules}
219
+ `;
220
+ }
221
+ /**
222
+ * Generates the fuller `llms-full.txt` (adds best-practices and known gaps) — the complete brief
223
+ * for an agent building with Stone.js.
224
+ *
225
+ * @param base - The knowledge base (defaults to the built-in one).
226
+ * @returns The `llms-full.txt` content.
227
+ */
228
+ function generateLlmsFullTxt(base = knowledgeBase) {
229
+ const bestPractices = base.bestPractices.map((b) => `- ${b.rule}\n - Why: ${b.why}`).join('\n');
230
+ const gaps = base.gaps.map((g) => `- **${g.name}** (${g.status}): ${g.note}`).join('\n');
231
+ return `${generateLlmsTxt(base)}
232
+ ## Best practices
233
+
234
+ ${bestPractices}
235
+
236
+ ## Known gaps (what to reach for a third party or the roadmap)
237
+
238
+ ${gaps}
239
+ `;
240
+ }
241
+
242
+ /**
243
+ * The Stone.js framework-knowledge tools served by `stone mcp`. They are registered on the MCP
244
+ * server automatically; point your coding agent at it and it can query the framework in real time
245
+ * (concepts, modules, best-practices, gaps) instead of scanning every package.
246
+ */
247
+ const stoneMcpTools = [
248
+ {
249
+ name: 'stone_search',
250
+ description: 'Search the Stone.js knowledge base (concepts, modules, best-practices, gaps).',
251
+ handler: (args) => searchKnowledge(String(args.query ?? ''))
252
+ },
253
+ {
254
+ name: 'stone_concept',
255
+ description: 'Explain a core Stone.js concept by id (omit id to list them all).',
256
+ handler: (args) => {
257
+ const id = String(args.id ?? '');
258
+ if (id.length === 0) {
259
+ return knowledgeBase.concepts.map((c) => ({ id: c.id, title: c.title }));
260
+ }
261
+ return getConcept(id) ?? { error: `Unknown concept: ${id}` };
262
+ }
263
+ },
264
+ {
265
+ name: 'stone_modules',
266
+ description: 'List the Stone.js ecosystem modules and what each does.',
267
+ handler: () => knowledgeBase.modules
268
+ },
269
+ {
270
+ name: 'stone_best_practices',
271
+ description: 'List Stone.js conventions and anti-patterns, each with its rationale.',
272
+ handler: () => knowledgeBase.bestPractices
273
+ },
274
+ {
275
+ name: 'stone_gaps',
276
+ description: 'List what Stone.js does not (yet) provide, and what to reach for instead.',
277
+ handler: () => knowledgeBase.gaps
278
+ },
279
+ {
280
+ name: 'stone_brief',
281
+ description: 'Return the full agent brief (llms-full.txt): concepts, modules, best-practices, gaps.',
282
+ handler: () => generateLlmsFullTxt()
283
+ }
284
+ ];
285
+ /**
286
+ * Creates tools that let an agent (or the developer through it) report a bug or request a feature
287
+ * as a real GitHub issue, straight from the dev loop.
288
+ *
289
+ * @param options - The GitHub token and target repository.
290
+ * @returns The report tools.
291
+ */
292
+ function createReportTools(options) {
293
+ const doFetch = options.fetch ?? fetch;
294
+ const openIssue = async (title, body, label) => {
295
+ const response = await doFetch(`https://api.github.com/repos/${options.repo}/issues`, {
296
+ method: 'POST',
297
+ headers: {
298
+ authorization: `Bearer ${options.token}`,
299
+ accept: 'application/vnd.github+json',
300
+ 'content-type': 'application/json'
301
+ },
302
+ body: JSON.stringify({ title, body, labels: [label] })
303
+ });
304
+ if (!response.ok) {
305
+ return { error: `GitHub API error: ${response.status}` };
306
+ }
307
+ const issue = await response.json();
308
+ return { number: issue.number, url: issue.html_url };
309
+ };
310
+ return [
311
+ {
312
+ name: 'stone_report_bug',
313
+ description: 'Open a bug report as a GitHub issue on the Stone.js repository.',
314
+ handler: async (args) => await openIssue(String(args.title ?? 'Bug report'), String(args.body ?? ''), 'bug')
315
+ },
316
+ {
317
+ name: 'stone_request_feature',
318
+ description: 'Open a feature request as a GitHub issue on the Stone.js repository.',
319
+ handler: async (args) => await openIssue(String(args.title ?? 'Feature request'), String(args.body ?? ''), 'enhancement')
320
+ }
321
+ ];
322
+ }
323
+
324
+ /**
325
+ * The platform tag the CLI adapter runs under. Re-declared locally (rather than imported) to keep
326
+ * this package decoupled from `@stone-js/node-cli-adapter`; the value must match the adapter's.
327
+ */
328
+ const NODE_CONSOLE_PLATFORM = 'node_console';
329
+ /** The default MCP server name when the app declares none. */
330
+ const DEFAULT_MCP_SERVER_NAME = 'stone-mcp-dev';
331
+ /** The default MCP server version. */
332
+ const DEFAULT_MCP_SERVER_VERSION = '0.0.0';
333
+ /**
334
+ * The default `instructions` advertised to the agent: what this server is and how to use it.
335
+ */
336
+ const DEFAULT_MCP_INSTRUCTIONS = [
337
+ 'This MCP server exposes the Stone.js framework knowledge to help you build on it.',
338
+ 'Use the `stone_*` tools to look up concepts, modules, best-practices, gaps and documentation',
339
+ 'links before writing Stone.js code, so your answers match the framework\'s actual conventions.',
340
+ 'Any additional tools are provided by the developer for this project.'
341
+ ].join(' ');
342
+
343
+ /**
344
+ * Wrap any handler return value as MCP text content (JSON for structured data).
345
+ *
346
+ * @param result - The value a tool handler returned.
347
+ * @returns The MCP content payload.
348
+ */
349
+ function toToolContent(result) {
350
+ const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
351
+ return { content: [{ type: 'text', text }] };
352
+ }
353
+ /**
354
+ * Create the activity logger. It writes to **stderr** (never stdout, which the stdio transport
355
+ * reserves for the JSON-RPC protocol); a no-op when `quiet` is set.
356
+ *
357
+ * @param quiet - Silence the log.
358
+ * @returns The logger.
359
+ */
360
+ function createStderrLogger(quiet = false) {
361
+ return (message) => {
362
+ if (!quiet) {
363
+ process.stderr.write(`${message}\n`);
364
+ }
365
+ };
366
+ }
367
+ /**
368
+ * Resolve the full tool list: the built-in framework-knowledge tools, the optional GitHub report
369
+ * tools, then the app's own tools.
370
+ *
371
+ * @param options - The dev-server options.
372
+ * @returns The merged tool list.
373
+ */
374
+ function resolveTools(options) {
375
+ return [
376
+ ...stoneMcpTools,
377
+ ...(options.report !== undefined ? createReportTools(options.report) : []),
378
+ ...(options.tools ?? [])
379
+ ];
380
+ }
381
+ /**
382
+ * Build the SDK callback for one tool: log the call to stderr, run the handler, log the outcome,
383
+ * and wrap the result (or the error) as MCP content.
384
+ *
385
+ * @param tool - The tool definition.
386
+ * @param log - The activity logger.
387
+ * @returns The SDK tool callback.
388
+ */
389
+ function createToolCallback(tool, log) {
390
+ return async (args) => {
391
+ const input = args ?? {};
392
+ log(`→ ${tool.name}(${JSON.stringify(input)})`);
393
+ try {
394
+ const result = await tool.handler(input);
395
+ log(`← ${tool.name} ok`);
396
+ return toToolContent(result);
397
+ }
398
+ catch (error) {
399
+ const message = error instanceof Error ? error.message : String(error);
400
+ log(`← ${tool.name} error: ${message}`);
401
+ return { ...toToolContent({ error: message }), isError: true };
402
+ }
403
+ };
404
+ }
405
+ /**
406
+ * Build a fully-configured MCP server: advertise the instructions and register every resolved tool
407
+ * with its logging callback. The handlers run in-process (dev/knowledge helpers, not the domain).
408
+ *
409
+ * @param options - The dev-server options.
410
+ * @param log - The activity logger.
411
+ * @returns The configured MCP server.
412
+ */
413
+ function buildMcpServer(options, log) {
414
+ const server = new McpServer({ name: options.name ?? DEFAULT_MCP_SERVER_NAME, version: options.version ?? DEFAULT_MCP_SERVER_VERSION }, { instructions: options.instructions ?? DEFAULT_MCP_INSTRUCTIONS });
415
+ for (const tool of resolveTools(options)) {
416
+ server.registerTool(tool.name, { description: tool.description, inputSchema: (tool.inputSchema ?? {}) }, createToolCallback(tool, log));
417
+ }
418
+ return server;
419
+ }
420
+ /**
421
+ * Start the MCP dev server over stdio and keep it alive until the process is interrupted.
422
+ *
423
+ * The stdio transport speaks JSON-RPC on stdout and keeps the event loop alive by reading stdin,
424
+ * so `Ctrl+C` (SIGINT) stops it, exactly like `stone dev`.
425
+ *
426
+ * @param options - The dev-server options.
427
+ */
428
+ /* v8 ignore start -- process lifecycle: stdio transport + signal handling, not unit-testable */
429
+ async function startMcpDevServer(options) {
430
+ const log = createStderrLogger(options.quiet);
431
+ const server = buildMcpServer(options, log);
432
+ const transport = new StdioServerTransport();
433
+ const shutdown = () => {
434
+ log('mcp: shutting down');
435
+ void server.close().finally(() => process.exit(0));
436
+ };
437
+ process.once('SIGINT', shutdown);
438
+ process.once('SIGTERM', shutdown);
439
+ if (!hasMcpJson(process.cwd())) {
440
+ log('mcp: no .mcp.json found — run `stone mcp --init` to register this server for your agent');
441
+ }
442
+ await server.connect(transport);
443
+ log(`mcp: ${resolveTools(options).length} tools ready on stdio — press Ctrl+C to stop`);
444
+ }
445
+ /* v8 ignore stop */
446
+
447
+ /** Keys whose values are redacted from any config dump (env secrets, credentials). */
448
+ const SECRET_KEY = /(secret|token|password|passwd|api[_-]?key|credential|private|passphrase|auth)/i;
449
+ /** How deep {@link sanitize} walks before bailing out. */
450
+ const MAX_DEPTH = 6;
451
+ /**
452
+ * Best-effort name of a module reference (class, function, or meta-module `{ module }`).
453
+ *
454
+ * @param value - The reference to name.
455
+ * @returns The resolved name.
456
+ */
457
+ function moduleName(value) {
458
+ if (value === undefined || value === null) {
459
+ return 'unknown';
460
+ }
461
+ if (typeof value === 'string') {
462
+ return value;
463
+ }
464
+ if (typeof value === 'function') {
465
+ return value.name.length > 0 ? value.name : 'anonymous';
466
+ }
467
+ if (typeof value === 'object') {
468
+ const meta = value;
469
+ if (meta.module !== undefined) {
470
+ return moduleName(meta.module);
471
+ }
472
+ if (typeof meta.name === 'string') {
473
+ return meta.name;
474
+ }
475
+ }
476
+ return 'unknown';
477
+ }
478
+ /**
479
+ * Produce a JSON-safe, secret-redacted copy of a config value: functions/classes become a label,
480
+ * `RegExp` its source, secret-looking keys `[redacted]`, and recursion is depth-capped.
481
+ *
482
+ * @param value - The value to sanitize.
483
+ * @param depth - The current recursion depth.
484
+ * @returns A serializable value.
485
+ */
486
+ function sanitize(value, depth = 0) {
487
+ if (depth > MAX_DEPTH) {
488
+ return '[max-depth]';
489
+ }
490
+ if (value === undefined || value === null) {
491
+ return value;
492
+ }
493
+ if (typeof value === 'function') {
494
+ return `[Function: ${value.name.length > 0 ? value.name : 'anonymous'}]`;
495
+ }
496
+ if (value instanceof RegExp) {
497
+ return value.toString();
498
+ }
499
+ if (Array.isArray(value)) {
500
+ return value.map((item) => sanitize(item, depth + 1));
501
+ }
502
+ if (typeof value === 'object') {
503
+ const out = {};
504
+ for (const [key, val] of Object.entries(value)) {
505
+ out[key] = SECRET_KEY.test(key) ? '[redacted]' : sanitize(val, depth + 1);
506
+ }
507
+ return out;
508
+ }
509
+ return value;
510
+ }
511
+ /** Remove `undefined` and empty arrays from an object, so tool output stays terse. */
512
+ function clean(obj) {
513
+ const out = {};
514
+ for (const [key, val] of Object.entries(obj)) {
515
+ if (val === undefined) {
516
+ continue;
517
+ }
518
+ if (Array.isArray(val) && val.length === 0) {
519
+ continue;
520
+ }
521
+ out[key] = val;
522
+ }
523
+ return out;
524
+ }
525
+ /** Map a route definition (and its children) to a terse, serializable shape. */
526
+ function mapRoute(def) {
527
+ const methods = def.methods ?? (def.method !== undefined ? [def.method] : undefined);
528
+ const children = def.children ?? [];
529
+ const middleware = def.middleware ?? [];
530
+ return clean({
531
+ name: def.name,
532
+ methods,
533
+ path: def.path,
534
+ handler: def.handler !== undefined ? moduleName(def.handler) : undefined,
535
+ middleware: middleware.map(moduleName),
536
+ children: children.map(mapRoute)
537
+ });
538
+ }
539
+ /** Count routes across the definition tree. */
540
+ function countRoutes(defs) {
541
+ return defs.reduce((total, def) => {
542
+ const children = def.children ?? [];
543
+ return total + 1 + countRoutes(children);
544
+ }, 0);
545
+ }
546
+ /**
547
+ * Build the read-only introspection tools bound to the app's resolved blueprint.
548
+ *
549
+ * These expose what the app actually declares (routes, commands, adapters, providers, kernel
550
+ * pipeline, config) so a coding agent understands *this* app, not just the framework. They read
551
+ * only, never mutate, and redact secret-looking config values.
552
+ *
553
+ * @param blueprint - The resolved application blueprint.
554
+ * @returns The introspection tools.
555
+ */
556
+ function createIntrospectionTools(blueprint) {
557
+ const routes = () => blueprint.get('stone.router.definitions', []);
558
+ const commands = () => blueprint.get('stone.adapter.commands', []);
559
+ return [
560
+ {
561
+ name: 'stone_app',
562
+ description: 'Summarize the current Stone.js app: name, env, active platform, and counts of routes/commands/providers/adapters.',
563
+ handler: () => clean({
564
+ name: blueprint.get('stone.name'),
565
+ env: blueprint.get('stone.env'),
566
+ platform: blueprint.get('stone.adapter.platform'),
567
+ counts: {
568
+ routes: countRoutes(routes()),
569
+ commands: commands().length,
570
+ providers: blueprint.get('stone.providers', []).length,
571
+ adapters: blueprint.get('stone.adapters', []).length
572
+ }
573
+ })
574
+ },
575
+ {
576
+ name: 'stone_routes',
577
+ description: 'List the app\'s routes (path, methods, name, handler, middleware) as declared on the router.',
578
+ handler: () => routes().map(mapRoute)
579
+ },
580
+ {
581
+ name: 'stone_commands',
582
+ description: 'List the app\'s CLI commands (name, alias, args, description).',
583
+ handler: () => commands().map((c) => clean({
584
+ name: c.options?.name,
585
+ alias: c.options?.alias,
586
+ args: c.options?.args,
587
+ desc: c.options?.desc
588
+ }))
589
+ },
590
+ {
591
+ name: 'stone_adapters',
592
+ description: 'List the registered adapters (platform, alias, default/current) and the active platform.',
593
+ handler: () => ({
594
+ active: blueprint.get('stone.adapter.platform'),
595
+ adapters: blueprint.get('stone.adapters', []).map((a) => clean({
596
+ platform: a.platform,
597
+ alias: a.alias,
598
+ current: a.current,
599
+ default: a.default
600
+ }))
601
+ })
602
+ },
603
+ {
604
+ name: 'stone_providers',
605
+ description: 'List the app\'s service providers.',
606
+ handler: () => blueprint.get('stone.providers', []).map(moduleName)
607
+ },
608
+ {
609
+ name: 'stone_kernel',
610
+ description: 'Show the kernel pipeline: the event handler, middleware, and registered error handlers.',
611
+ handler: () => {
612
+ const kernel = blueprint.get('stone.kernel', {});
613
+ return clean({
614
+ eventHandler: kernel.eventHandler !== undefined ? moduleName(kernel.eventHandler) : undefined,
615
+ middleware: (kernel.middleware ?? []).map(moduleName),
616
+ errorHandlers: Object.keys(kernel.errorHandlers ?? {})
617
+ });
618
+ }
619
+ },
620
+ {
621
+ name: 'stone_key_routes',
622
+ description: 'List the key-routing definitions (event-bus / realtime / keyed events): key to handler.',
623
+ handler: () => blueprint.get('stone.keyRouting.definitions', []).map((d) => clean({
624
+ key: d.key,
625
+ action: d.action,
626
+ handler: d.module !== undefined ? moduleName(d.module) : undefined
627
+ }))
628
+ },
629
+ {
630
+ name: 'stone_config',
631
+ description: 'Read a resolved config value by dotted key under `stone.*` (secrets redacted). Omit `key` to list the top-level `stone` keys.',
632
+ handler: (args) => {
633
+ const key = String(args.key ?? '');
634
+ if (key.length === 0) {
635
+ return Object.keys(blueprint.get('stone', {}));
636
+ }
637
+ return sanitize(blueprint.get(key));
638
+ }
639
+ }
640
+ ];
641
+ }
642
+
643
+ /**
644
+ * Custom error for the MCP dev module.
645
+ */
646
+ class McpDevError extends RuntimeError {
647
+ constructor(message, options = {}) {
648
+ super(message, options);
649
+ this.name = 'McpDevError';
650
+ }
651
+ }
652
+
653
+ /**
654
+ * Configuration for the `mcp` command.
655
+ */
656
+ const mcpCommandOptions = {
657
+ name: 'mcp',
658
+ alias: 'm',
659
+ desc: 'Start an MCP server (stdio) exposing Stone.js knowledge + your app + your tools to a coding agent',
660
+ options: (yargs) => {
661
+ return yargs
662
+ .option('init', { type: 'boolean', desc: 'Register this server in .mcp.json (create/merge) and exit' })
663
+ .option('name', { type: 'string', desc: 'Override the MCP server name' })
664
+ .option('quiet', { type: 'boolean', desc: 'Silence the stderr activity log' });
665
+ }
666
+ };
667
+ /**
668
+ * Starts the MCP dev server from the `stone mcp` command.
669
+ *
670
+ * It reads `stone.mcpDev` from the blueprint (server name, instructions, your tools) and lets the
671
+ * MCP SDK own the protocol and tool execution: framework knowledge helpers do not need to traverse
672
+ * the kernel. `--name` / `--quiet` flags override the configured values.
673
+ */
674
+ class McpCommand {
675
+ container;
676
+ /**
677
+ * @param container - The dependency injection container.
678
+ * @throws {McpDevError} If the container is not provided.
679
+ */
680
+ constructor(container) {
681
+ this.container = container;
682
+ if (container === undefined) {
683
+ throw new McpDevError('Container is required to create a McpCommand instance.');
684
+ }
685
+ }
686
+ /**
687
+ * Handle the `mcp` command: start the server and keep it running until interrupted.
688
+ *
689
+ * @param event - The incoming CLI event carrying the parsed flags.
690
+ */
691
+ async handle(event) {
692
+ const blueprint = this.container.make('blueprint');
693
+ if (event.getMetadataValue('init', false)) {
694
+ const { file, changed } = initMcpJson(process.cwd());
695
+ process.stderr.write(changed ? `mcp: registered this server in ${file}\n` : `mcp: ${file} already registers this server\n`);
696
+ return;
697
+ }
698
+ const options = blueprint.get('stone.mcpDev', {});
699
+ const name = event.getMetadataValue('name', options.name);
700
+ const quiet = event.getMetadataValue('quiet', options.quiet ?? false);
701
+ const tools = [...createIntrospectionTools(blueprint), ...(options.tools ?? [])];
702
+ await startMcpDevServer({ ...options, name, quiet, tools });
703
+ }
704
+ }
705
+
706
+ /**
707
+ * Middleware that registers the `mcp` command when the app runs on the Node CLI adapter.
708
+ *
709
+ * It mirrors the router's command registration: contribute a `MetaCommandHandler` to
710
+ * `stone.adapter.commands` so the CLI (itself a Stone.js app on the Node CLI adapter) discovers
711
+ * `stone mcp` by introspection, no hard-coding.
712
+ *
713
+ * @param context - The blueprint context.
714
+ * @param next - The next pipeline function.
715
+ * @returns The updated blueprint.
716
+ */
717
+ const SetMcpCommandsMiddleware = async (context, next) => {
718
+ if (context.blueprint.get('stone.adapter.platform') === NODE_CONSOLE_PLATFORM) {
719
+ context.blueprint.add('stone.adapter.commands', [{ options: mcpCommandOptions, isClass: true, module: McpCommand }]);
720
+ }
721
+ return await next(context);
722
+ };
723
+ /**
724
+ * The blueprint middleware contributed by the MCP dev module.
725
+ */
726
+ const metaMcpDevBlueprintMiddleware = [
727
+ { module: SetMcpCommandsMiddleware, priority: 5 }
728
+ ];
729
+
730
+ /**
731
+ * Opt-in blueprint: import and register it to add the `stone mcp` command.
732
+ *
733
+ * It contributes a blueprint middleware that registers the command on the Node CLI adapter. Add
734
+ * your own tools and the server name/instructions under `stone.mcpDev` (or via `@McpDev()` /
735
+ * `defineMcpDev()`).
736
+ */
737
+ const mcpDevBlueprint = {
738
+ stone: {
739
+ blueprint: {
740
+ middleware: metaMcpDevBlueprintMiddleware
741
+ },
742
+ mcpDev: {
743
+ tools: []
744
+ }
745
+ }
746
+ };
747
+ /**
748
+ * Imperative helper: build an MCP dev blueprint with the given options.
749
+ *
750
+ * @param options - The MCP dev options (server name, instructions, your tools, report tools).
751
+ * @returns The blueprint to register in your app.
752
+ */
753
+ function defineMcpDev(options = {}) {
754
+ return {
755
+ stone: {
756
+ blueprint: {
757
+ middleware: metaMcpDevBlueprintMiddleware
758
+ },
759
+ mcpDev: options
760
+ }
761
+ };
762
+ }
763
+
764
+ /**
765
+ * A class decorator that adds the `stone mcp` command to your app.
766
+ *
767
+ * Declarative counterpart of registering {@link mcpDevBlueprint}: apply it to your app class to
768
+ * expose the framework-knowledge tools (plus any you declare) to a coding agent over MCP.
769
+ *
770
+ * @param options - The MCP dev options (server name, instructions, your tools).
771
+ * @returns A class decorator.
772
+ *
773
+ * @example
774
+ * ```typescript
775
+ * @McpDev({ tools: [myTool] })
776
+ * @StoneApp({ name: 'my-app' })
777
+ * export class Application {}
778
+ * ```
779
+ */
780
+ const McpDev = (options = {}) => {
781
+ return classDecoratorLegacyWrapper((target, context) => {
782
+ addBlueprint(target, context, mcpDevBlueprint, { stone: { mcpDev: options } });
783
+ });
784
+ };
785
+
786
+ export { DEFAULT_MCP_INSTRUCTIONS, DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION, McpCommand, McpDev, McpDevError, NODE_CONSOLE_PLATFORM, SetMcpCommandsMiddleware, buildMcpServer, createIntrospectionTools, createReportTools, createStderrLogger, createToolCallback, defineMcpDev, generateLlmsFullTxt, generateLlmsTxt, getConcept, hasMcpJson, initMcpJson, knowledgeBase, mcpCommandOptions, mcpDevBlueprint, mcpServerEntry, mergeMcpJson, metaMcpDevBlueprintMiddleware, moduleName, resolveTools, sanitize, searchKnowledge, startMcpDevServer, stoneMcpTools, toToolContent };