@pugi/cli 0.1.0-beta.4 → 0.1.0-beta.40

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 (249) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -25
  3. package/bin/run.js +33 -1
  4. package/dist/commands/jobs-watch.js +201 -0
  5. package/dist/commands/jobs.js +15 -0
  6. package/dist/commands/smoke.js +133 -0
  7. package/dist/core/agent-progress/cleanup.js +134 -0
  8. package/dist/core/agent-progress/schema.js +144 -0
  9. package/dist/core/agent-progress/writer.js +101 -0
  10. package/dist/core/artifact-chain/dispatcher.js +148 -0
  11. package/dist/core/artifact-chain/exporter.js +164 -0
  12. package/dist/core/artifact-chain/state.js +243 -0
  13. package/dist/core/artifact-chain/steps.js +169 -0
  14. package/dist/core/auth/ensure-authenticated.js +129 -0
  15. package/dist/core/auth/env-provider.js +238 -0
  16. package/dist/core/auto-update/channels.js +122 -0
  17. package/dist/core/auto-update/checker.js +241 -0
  18. package/dist/core/auto-update/state.js +235 -0
  19. package/dist/core/bare-mode/index.js +107 -0
  20. package/dist/core/bash-classifier.js +108 -1
  21. package/dist/core/checkpoint/resumer.js +149 -0
  22. package/dist/core/checkpoint/rewinder.js +291 -0
  23. package/dist/core/codegraph/decision-store.js +248 -0
  24. package/dist/core/codegraph/detect-repo.js +459 -0
  25. package/dist/core/codegraph/install.js +134 -0
  26. package/dist/core/codegraph/offer-hook.js +220 -0
  27. package/dist/core/compact/auto-trigger.js +96 -0
  28. package/dist/core/compact/buffer-rewriter.js +115 -0
  29. package/dist/core/compact/summarizer.js +208 -0
  30. package/dist/core/compact/token-counter.js +108 -0
  31. package/dist/core/consensus/diff-capture.js +73 -0
  32. package/dist/core/context/index.js +7 -0
  33. package/dist/core/context/markdown-traverse.js +255 -0
  34. package/dist/core/cost/rate-card.js +129 -0
  35. package/dist/core/cost/tracker.js +221 -0
  36. package/dist/core/denial-tracking/index.js +8 -0
  37. package/dist/core/denial-tracking/state.js +264 -0
  38. package/dist/core/diagnostics/probe-runner.js +93 -0
  39. package/dist/core/diagnostics/probes/api.js +46 -0
  40. package/dist/core/diagnostics/probes/auth.js +86 -0
  41. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  42. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  43. package/dist/core/diagnostics/probes/config.js +72 -0
  44. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  45. package/dist/core/diagnostics/probes/disk.js +81 -0
  46. package/dist/core/diagnostics/probes/git.js +65 -0
  47. package/dist/core/diagnostics/probes/mcp.js +75 -0
  48. package/dist/core/diagnostics/probes/node.js +59 -0
  49. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  50. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  51. package/dist/core/diagnostics/probes/session.js +74 -0
  52. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  53. package/dist/core/diagnostics/probes/workspace.js +63 -0
  54. package/dist/core/diagnostics/types.js +70 -0
  55. package/dist/core/dispatch/cache-cleanup.js +197 -0
  56. package/dist/core/dispatch/cache-handoff.js +295 -0
  57. package/dist/core/edits/dispatch.js +218 -2
  58. package/dist/core/edits/journal.js +199 -0
  59. package/dist/core/edits/layer-d-ast.js +557 -14
  60. package/dist/core/edits/verify-hook.js +273 -0
  61. package/dist/core/edits/worktree.js +322 -0
  62. package/dist/core/engine/anvil-client.js +115 -5
  63. package/dist/core/engine/budgets.js +98 -0
  64. package/dist/core/engine/context-prefix.js +155 -0
  65. package/dist/core/engine/intent.js +260 -0
  66. package/dist/core/engine/native-pugi.js +860 -211
  67. package/dist/core/engine/prompts.js +88 -2
  68. package/dist/core/engine/strip-internal-fields.js +124 -0
  69. package/dist/core/engine/tool-bridge.js +992 -36
  70. package/dist/core/feedback/queue.js +177 -0
  71. package/dist/core/feedback/submitter.js +145 -0
  72. package/dist/core/file-cache.js +113 -1
  73. package/dist/core/hooks/events.js +44 -0
  74. package/dist/core/hooks/index.js +15 -0
  75. package/dist/core/hooks/registry.js +213 -0
  76. package/dist/core/hooks/runner.js +236 -0
  77. package/dist/core/hooks/v2/event-emitter.js +115 -0
  78. package/dist/core/hooks/v2/executor.js +282 -0
  79. package/dist/core/hooks/v2/index.js +25 -0
  80. package/dist/core/hooks/v2/lifecycle.js +104 -0
  81. package/dist/core/hooks/v2/loader.js +216 -0
  82. package/dist/core/hooks/v2/matcher.js +125 -0
  83. package/dist/core/hooks/v2/trust.js +143 -0
  84. package/dist/core/hooks/v2/types.js +86 -0
  85. package/dist/core/lsp/cache.js +105 -0
  86. package/dist/core/lsp/client.js +776 -0
  87. package/dist/core/lsp/language-detect.js +66 -0
  88. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  89. package/dist/core/mcp/client.js +75 -6
  90. package/dist/core/mcp/http-server.js +553 -0
  91. package/dist/core/mcp/orchestrator-tools.js +662 -0
  92. package/dist/core/mcp/permission.js +190 -0
  93. package/dist/core/mcp/registry.js +24 -2
  94. package/dist/core/mcp/server-tools.js +219 -0
  95. package/dist/core/mcp/server.js +397 -0
  96. package/dist/core/memory/dual-write.js +416 -0
  97. package/dist/core/memory/phase1-kinds.js +20 -0
  98. package/dist/core/memory-sync/queue.js +158 -0
  99. package/dist/core/onboarding/ensure-initialized.js +133 -0
  100. package/dist/core/onboarding/marker.js +111 -0
  101. package/dist/core/onboarding/telemetry-state.js +108 -0
  102. package/dist/core/output-style/presets.js +176 -0
  103. package/dist/core/output-style/state.js +185 -0
  104. package/dist/core/permissions/auto-classifier.js +124 -0
  105. package/dist/core/permissions/circuit-breaker.js +83 -0
  106. package/dist/core/permissions/gate.js +278 -0
  107. package/dist/core/permissions/index.js +20 -0
  108. package/dist/core/permissions/mode.js +174 -0
  109. package/dist/core/permissions/state.js +241 -0
  110. package/dist/core/permissions/tool-class.js +93 -0
  111. package/dist/core/prd-check/parser.js +215 -0
  112. package/dist/core/prd-check/reporter.js +127 -0
  113. package/dist/core/prd-check/session-review.js +557 -0
  114. package/dist/core/prd-check/verifiers.js +223 -0
  115. package/dist/core/pugi-md/context-injector.js +76 -0
  116. package/dist/core/pugi-md/walk-up.js +207 -0
  117. package/dist/core/release-notes/parser.js +241 -0
  118. package/dist/core/release-notes/state.js +116 -0
  119. package/dist/core/repl/history.js +11 -1
  120. package/dist/core/repl/model-pricing.js +135 -0
  121. package/dist/core/repl/session.js +1899 -38
  122. package/dist/core/repl/slash-commands.js +406 -21
  123. package/dist/core/repl/store/session-store.js +31 -2
  124. package/dist/core/repl/workspace-context.js +22 -0
  125. package/dist/core/repo-map/build.js +125 -0
  126. package/dist/core/repo-map/cache.js +185 -0
  127. package/dist/core/repo-map/extractor.js +254 -0
  128. package/dist/core/repo-map/formatter.js +145 -0
  129. package/dist/core/repo-map/scanner.js +211 -0
  130. package/dist/core/retry-budget/budget.js +284 -0
  131. package/dist/core/retry-budget/index.js +5 -0
  132. package/dist/core/session.js +92 -0
  133. package/dist/core/settings.js +80 -0
  134. package/dist/core/share/formatter.js +271 -0
  135. package/dist/core/share/redactor.js +221 -0
  136. package/dist/core/share/uploader.js +267 -0
  137. package/dist/core/skills/defaults.js +457 -0
  138. package/dist/core/smoke/headless-driver.js +174 -0
  139. package/dist/core/smoke/orchestrator.js +194 -0
  140. package/dist/core/smoke/runner.js +238 -0
  141. package/dist/core/smoke/scenario-parser.js +316 -0
  142. package/dist/core/subagents/dispatcher-real.js +600 -0
  143. package/dist/core/subagents/dispatcher.js +113 -24
  144. package/dist/core/subagents/index.js +18 -5
  145. package/dist/core/subagents/isolation-matrix.js +213 -0
  146. package/dist/core/subagents/spawn.js +19 -4
  147. package/dist/core/telemetry/emitter.js +229 -0
  148. package/dist/core/telemetry/queue.js +251 -0
  149. package/dist/core/theme/context.js +91 -0
  150. package/dist/core/theme/presets.js +228 -0
  151. package/dist/core/theme/state.js +181 -0
  152. package/dist/core/todos/invariant.js +10 -0
  153. package/dist/core/todos/state.js +177 -0
  154. package/dist/core/transport/version-interceptor.js +166 -0
  155. package/dist/core/vim/keymap.js +288 -0
  156. package/dist/core/vim/state.js +92 -0
  157. package/dist/index.js +28 -0
  158. package/dist/runtime/bootstrap.js +190 -0
  159. package/dist/runtime/cli.js +3073 -321
  160. package/dist/runtime/commands/cancel.js +231 -0
  161. package/dist/runtime/commands/chain.js +489 -0
  162. package/dist/runtime/commands/codegraph-status.js +227 -0
  163. package/dist/runtime/commands/compact.js +297 -0
  164. package/dist/runtime/commands/cost.js +199 -0
  165. package/dist/runtime/commands/delegate.js +242 -11
  166. package/dist/runtime/commands/dispatch.js +126 -0
  167. package/dist/runtime/commands/doctor.js +390 -0
  168. package/dist/runtime/commands/feedback.js +184 -0
  169. package/dist/runtime/commands/hooks.js +184 -0
  170. package/dist/runtime/commands/lsp.js +368 -0
  171. package/dist/runtime/commands/mcp.js +879 -0
  172. package/dist/runtime/commands/memory.js +508 -0
  173. package/dist/runtime/commands/model.js +237 -0
  174. package/dist/runtime/commands/onboarding.js +275 -0
  175. package/dist/runtime/commands/patch.js +128 -0
  176. package/dist/runtime/commands/permissions.js +112 -0
  177. package/dist/runtime/commands/plan.js +143 -0
  178. package/dist/runtime/commands/prd-check.js +285 -0
  179. package/dist/runtime/commands/redo-blob-store.js +92 -0
  180. package/dist/runtime/commands/redo.js +361 -0
  181. package/dist/runtime/commands/release-notes.js +229 -0
  182. package/dist/runtime/commands/repo-map.js +95 -0
  183. package/dist/runtime/commands/report.js +299 -0
  184. package/dist/runtime/commands/resume.js +118 -0
  185. package/dist/runtime/commands/review-consensus.js +17 -2
  186. package/dist/runtime/commands/rewind.js +333 -0
  187. package/dist/runtime/commands/sessions.js +163 -0
  188. package/dist/runtime/commands/share.js +316 -0
  189. package/dist/runtime/commands/status.js +186 -0
  190. package/dist/runtime/commands/stickers.js +82 -0
  191. package/dist/runtime/commands/style.js +194 -0
  192. package/dist/runtime/commands/theme.js +196 -0
  193. package/dist/runtime/commands/undo.js +32 -0
  194. package/dist/runtime/commands/update.js +289 -0
  195. package/dist/runtime/commands/vim.js +140 -0
  196. package/dist/runtime/commands/worktree.js +177 -0
  197. package/dist/runtime/headless-repl.js +195 -0
  198. package/dist/runtime/headless.js +543 -0
  199. package/dist/runtime/load-hooks-or-exit.js +71 -0
  200. package/dist/runtime/plan-decompose.js +531 -0
  201. package/dist/runtime/version.js +65 -0
  202. package/dist/tools/agent-tool.js +229 -0
  203. package/dist/tools/apply-patch.js +556 -0
  204. package/dist/tools/ask-user-question.js +213 -0
  205. package/dist/tools/ask-user.js +115 -0
  206. package/dist/tools/file-tools.js +85 -14
  207. package/dist/tools/lsp-tools.js +189 -0
  208. package/dist/tools/mcp-tool.js +260 -0
  209. package/dist/tools/multi-edit.js +361 -0
  210. package/dist/tools/registry.js +46 -0
  211. package/dist/tools/skill-tool.js +96 -0
  212. package/dist/tools/tasks.js +208 -0
  213. package/dist/tools/todo-write.js +184 -0
  214. package/dist/tools/web-fetch.js +147 -2
  215. package/dist/tools/web-search.js +458 -0
  216. package/dist/tui/agent-progress-card.js +111 -0
  217. package/dist/tui/agent-tree.js +10 -0
  218. package/dist/tui/ask-modal.js +2 -2
  219. package/dist/tui/ask-user-question-prompt.js +192 -0
  220. package/dist/tui/compact-banner.js +81 -0
  221. package/dist/tui/conversation-pane.js +82 -8
  222. package/dist/tui/cost-table.js +111 -0
  223. package/dist/tui/doctor-table.js +46 -0
  224. package/dist/tui/feedback-prompt.js +156 -0
  225. package/dist/tui/input-box.js +69 -2
  226. package/dist/tui/markdown-render.js +4 -4
  227. package/dist/tui/onboarding-wizard.js +240 -0
  228. package/dist/tui/permissions-picker.js +86 -0
  229. package/dist/tui/render.js +35 -0
  230. package/dist/tui/repl-render.js +303 -13
  231. package/dist/tui/repl-splash.js +2 -2
  232. package/dist/tui/repl.js +72 -14
  233. package/dist/tui/splash.js +1 -1
  234. package/dist/tui/status-bar.js +94 -16
  235. package/dist/tui/status-table.js +7 -0
  236. package/dist/tui/stickers-art.js +136 -0
  237. package/dist/tui/style-table.js +28 -0
  238. package/dist/tui/theme-table.js +29 -0
  239. package/dist/tui/tool-stream-pane.js +52 -3
  240. package/dist/tui/update-banner.js +20 -2
  241. package/dist/tui/vim-input.js +267 -0
  242. package/docs/examples/codegraph.mcp.json +10 -0
  243. package/package.json +12 -6
  244. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  245. package/test/scenarios/compact-force.scenario.txt +11 -0
  246. package/test/scenarios/identity.scenario.txt +11 -0
  247. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  248. package/test/scenarios/walkback.scenario.txt +12 -0
  249. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,397 @@
1
+ import { EventEmitter } from 'node:events';
2
+ /**
3
+ * Pugi MCP server (β4 M2) — exposes Pugi's native tool surface to other
4
+ * agents (Claude Code, OpenCode, Codex CLI, any client that speaks
5
+ * MCP).
6
+ *
7
+ * Transport-agnostic core. The stdio entry-point lives at the bottom of
8
+ * this module (`serveStdio`); the HTTP+SSE wrapper lives in
9
+ * `./http-server.ts` and feeds the same core via the synchronous
10
+ * `handleMessage` entry-point.
11
+ *
12
+ * Spec: https://modelcontextprotocol.io/specification (2024-11-05).
13
+ *
14
+ * Methods implemented:
15
+ * - `initialize` -> protocol handshake, server capabilities
16
+ * - `notifications/initialized` -> ack, no-op
17
+ * - `tools/list` -> Pugi tool schemas
18
+ * - `tools/call` -> dispatch to the underlying executor
19
+ * - `ping` -> liveness, returns `{}` (some MCP clients
20
+ * poll this on a HTTP transport)
21
+ *
22
+ * NOT yet implemented (deferred — these are not on the β4 acceptance
23
+ * surface):
24
+ * - `resources/*` — file resource browser
25
+ * - `prompts/*` — server-supplied prompts
26
+ * - `sampling/*` — agent sampling callbacks
27
+ * - `notifications/cancelled` — client-side cancellation (we honour
28
+ * the AbortSignal passed at construction
29
+ * instead)
30
+ *
31
+ * Why hand-rolled instead of `@modelcontextprotocol/sdk`:
32
+ * - The SDK ships every transport (stdio, HTTP, WebSocket, SSE) and
33
+ * drags in a 3 MB compressed dependency footprint. Pugi-CLI ships as
34
+ * `npm i -g pugi` and every transitive dep is supply-chain risk.
35
+ * - The core JSON-RPC envelope is <500 LOC and we already maintain the
36
+ * client-side variant in `./client.ts` — keeping the server matching
37
+ * hand-roll lets a code reviewer hold both sides of the wire in one
38
+ * head.
39
+ * - Pugi-specific extensions (permission FSM hooks on `tools/call`,
40
+ * bearer-auth attestation on HTTP, scope-limited tool filtering for
41
+ * paired-agent worktrees) drop in cleanly because we own the
42
+ * dispatch table.
43
+ */
44
+ /* ---------- protocol types (mirror client.ts conventions) ----------------- */
45
+ export const PUGI_MCP_PROTOCOL_VERSION = '2024-11-05';
46
+ export const PUGI_MCP_SERVER_NAME = 'pugi';
47
+ export const PUGI_MCP_SERVER_VERSION = '0.1.0';
48
+ /** JSON-RPC standard error codes used by the server. */
49
+ export const MCP_ERROR_CODES = Object.freeze({
50
+ PARSE_ERROR: -32700,
51
+ INVALID_REQUEST: -32600,
52
+ METHOD_NOT_FOUND: -32601,
53
+ INVALID_PARAMS: -32602,
54
+ INTERNAL_ERROR: -32603,
55
+ /**
56
+ * Pugi-specific: operator-side permission gate refused this tool call.
57
+ * Distinct from generic INTERNAL_ERROR so MCP clients can surface a
58
+ * "permission denied" status to their user.
59
+ */
60
+ PERMISSION_REFUSED: -32001,
61
+ /**
62
+ * Pugi-specific: HTTP transport saw an invalid or missing bearer
63
+ * token. Stdio transport never emits this code (no auth).
64
+ */
65
+ AUTH_REQUIRED: -32002,
66
+ });
67
+ export class McpServerToolError extends Error {
68
+ code;
69
+ constructor(message, code = MCP_ERROR_CODES.INTERNAL_ERROR) {
70
+ super(message);
71
+ this.name = 'McpServerToolError';
72
+ this.code = code;
73
+ }
74
+ }
75
+ /**
76
+ * Build a Pugi MCP server bound to a fixed tool surface. Stateless
77
+ * between requests — the same instance can drive many concurrent stdio
78
+ * connections (one per child process) without cross-talk.
79
+ */
80
+ export function createPugiMcpServer(options) {
81
+ const events = new EventEmitter();
82
+ // Build a lookup by name once at construction. Duplicate names are a
83
+ // programmer error and we throw eagerly so the bug surfaces in the
84
+ // boot path, not at the first `tools/call`.
85
+ const byName = new Map();
86
+ for (const tool of options.tools) {
87
+ // Reject names that would re-encode as MCP `mcp__<server>__<tool>`
88
+ // false-positively on the consumer side — a server name containing
89
+ // `__` makes parseMcpToolName misalign the split. We surface the
90
+ // bug here, not at first dispatch. β4 r1 P2.
91
+ if (tool.name.includes('__')) {
92
+ throw new Error(`pugi mcp server: tool name "${tool.name}" must not contain "__" (collides with mcp__<server>__<tool> naming)`);
93
+ }
94
+ if (byName.has(tool.name)) {
95
+ throw new Error(`pugi mcp server: duplicate tool name "${tool.name}" — every tool in the surface must be unique`);
96
+ }
97
+ byName.set(tool.name, tool);
98
+ }
99
+ const tools = Array.from(byName.values()).sort((a, b) => a.name.localeCompare(b.name));
100
+ const log = options.log ?? (() => { });
101
+ if (typeof options.permissionGate !== 'function') {
102
+ throw new Error('pugi mcp server: options.permissionGate is required (β4 r1 P1 #2 — no implicit allow-all default)');
103
+ }
104
+ const permissionGate = options.permissionGate;
105
+ const requireInitialized = options.requireInitialized !== false;
106
+ let initialized = false;
107
+ async function dispatch(method, params, clientId) {
108
+ switch (method) {
109
+ case 'initialize': {
110
+ // The MCP spec permits multiple `initialize` calls before
111
+ // `notifications/initialized` settles the handshake — we just
112
+ // re-affirm. After init, repeated `initialize` is harmless on
113
+ // our side and lets the client recover from a state desync.
114
+ return {
115
+ protocolVersion: PUGI_MCP_PROTOCOL_VERSION,
116
+ capabilities: {
117
+ tools: { listChanged: false },
118
+ },
119
+ serverInfo: {
120
+ name: PUGI_MCP_SERVER_NAME,
121
+ version: PUGI_MCP_SERVER_VERSION,
122
+ },
123
+ };
124
+ }
125
+ case 'ping': {
126
+ return {};
127
+ }
128
+ case 'tools/list': {
129
+ return {
130
+ tools: tools.map((tool) => ({
131
+ name: tool.name,
132
+ description: tool.description,
133
+ inputSchema: tool.inputSchema,
134
+ })),
135
+ };
136
+ }
137
+ case 'tools/call': {
138
+ if (options.signal?.aborted) {
139
+ throw new McpServerToolError('server shutting down', MCP_ERROR_CODES.INTERNAL_ERROR);
140
+ }
141
+ // β4 r1 P2 — require `initialize` + `notifications/initialized`
142
+ // handshake first. Without this, an attacker with the bearer
143
+ // token can dispatch a `tools/call` without ever advertising
144
+ // client capabilities; the MCP spec mandates the handshake.
145
+ if (requireInitialized && !initialized) {
146
+ throw new McpServerToolError('tools/call: MCP handshake not complete — send `initialize` + `notifications/initialized` first', MCP_ERROR_CODES.INVALID_REQUEST);
147
+ }
148
+ const p = (params ?? {});
149
+ const rawName = p['name'];
150
+ if (typeof rawName !== 'string') {
151
+ throw new McpServerToolError('tools/call: params.name (string) is required', MCP_ERROR_CODES.INVALID_PARAMS);
152
+ }
153
+ // Trim whitespace-only names so the lookup fails fast with
154
+ // METHOD_NOT_FOUND instead of silently matching an entry that
155
+ // happens to share leading/trailing whitespace. β4 r1 P2.
156
+ const toolName = rawName.trim();
157
+ if (toolName.length === 0) {
158
+ throw new McpServerToolError('tools/call: params.name (string) is required', MCP_ERROR_CODES.INVALID_PARAMS);
159
+ }
160
+ const tool = byName.get(toolName);
161
+ if (!tool) {
162
+ throw new McpServerToolError(`tools/call: tool "${toolName}" is not registered`, MCP_ERROR_CODES.METHOD_NOT_FOUND);
163
+ }
164
+ const rawArgs = p['arguments'];
165
+ let args;
166
+ if (rawArgs === undefined || rawArgs === null) {
167
+ args = {};
168
+ }
169
+ else if (typeof rawArgs === 'object' && !Array.isArray(rawArgs)) {
170
+ args = rawArgs;
171
+ }
172
+ else {
173
+ throw new McpServerToolError('tools/call: params.arguments must be a JSON object', MCP_ERROR_CODES.INVALID_PARAMS);
174
+ }
175
+ const allowed = await permissionGate({
176
+ tool,
177
+ arguments: args,
178
+ ...(clientId ? { clientId } : {}),
179
+ });
180
+ if (!allowed) {
181
+ log('warn', `permission refused: ${tool.name}`);
182
+ throw new McpServerToolError(`permission refused by operator: ${tool.name}`, MCP_ERROR_CODES.PERMISSION_REFUSED);
183
+ }
184
+ // Stamp clientId on emitted events so the HTTP transport can
185
+ // route them to the originating SSE listener instead of
186
+ // broadcasting to every bearer-holder (β4 r1 P1 #5).
187
+ events.emit('tool_call', {
188
+ name: tool.name,
189
+ args,
190
+ ...(clientId ? { clientId } : {}),
191
+ });
192
+ try {
193
+ const text = await tool.execute(args);
194
+ events.emit('tool_result', {
195
+ name: tool.name,
196
+ ok: true,
197
+ summary: text.slice(0, 200),
198
+ ...(clientId ? { clientId } : {}),
199
+ });
200
+ return {
201
+ content: [{ type: 'text', text }],
202
+ isError: false,
203
+ };
204
+ }
205
+ catch (error) {
206
+ const message = error instanceof Error ? error.message : String(error);
207
+ events.emit('tool_result', {
208
+ name: tool.name,
209
+ ok: false,
210
+ summary: message.slice(0, 200),
211
+ ...(clientId ? { clientId } : {}),
212
+ });
213
+ // Tool-level failures surface as MCP `isError: true` content —
214
+ // the client knows the call ran but failed, distinct from a
215
+ // protocol-level error (bad tool name, malformed params).
216
+ return {
217
+ content: [{ type: 'text', text: message }],
218
+ isError: true,
219
+ };
220
+ }
221
+ }
222
+ case 'notifications/initialized': {
223
+ initialized = true;
224
+ events.emit('initialized');
225
+ return null;
226
+ }
227
+ default: {
228
+ if (method.startsWith('notifications/')) {
229
+ // Silently accept unknown notifications — the spec requires
230
+ // they be ignored, not errored.
231
+ return null;
232
+ }
233
+ throw new McpServerToolError(`method not found: ${method}`, MCP_ERROR_CODES.METHOD_NOT_FOUND);
234
+ }
235
+ }
236
+ }
237
+ async function handleMessage(request) {
238
+ const clientId = request.meta?.clientId;
239
+ // Notifications never get a response.
240
+ const isNotification = request.id === undefined || request.id === null;
241
+ if (isNotification) {
242
+ try {
243
+ await dispatch(request.method, request.params, clientId);
244
+ }
245
+ catch (error) {
246
+ log('error', `notification handler threw: ${error.message}`);
247
+ }
248
+ return null;
249
+ }
250
+ const id = request.id;
251
+ try {
252
+ const result = await dispatch(request.method, request.params, clientId);
253
+ // dispatch may return null for void responses (notifications) —
254
+ // but we already filtered those above. Treat null here as `{}`.
255
+ return {
256
+ jsonrpc: '2.0',
257
+ id,
258
+ result: result ?? {},
259
+ };
260
+ }
261
+ catch (error) {
262
+ const isToolError = error instanceof McpServerToolError;
263
+ const code = isToolError ? error.code : MCP_ERROR_CODES.INTERNAL_ERROR;
264
+ const message = error instanceof Error ? error.message : String(error);
265
+ log('error', `dispatch ${request.method} (id=${id}) failed: ${message}`);
266
+ return {
267
+ jsonrpc: '2.0',
268
+ id,
269
+ error: { code, message },
270
+ };
271
+ }
272
+ }
273
+ return {
274
+ handleMessage,
275
+ events,
276
+ listToolsSync() {
277
+ return tools.slice();
278
+ },
279
+ // Internal: exposed to tests via type assertion when they want to
280
+ // verify the initialized flag advanced. Not part of the public
281
+ // interface intentionally — production callers should listen on
282
+ // `events` instead.
283
+ // @ts-expect-error — debug accessor
284
+ _isInitialized() {
285
+ return initialized;
286
+ },
287
+ };
288
+ }
289
+ /**
290
+ * Run the server on stdio. Reads one JSON-RPC line per request from
291
+ * stdin, writes the response (or nothing for notifications) as one line
292
+ * to stdout. Lines are `\n`-terminated UTF-8 JSON.
293
+ *
294
+ * Returns a promise that resolves when stdin closes. The caller can race
295
+ * it against `signal` to force shutdown — the stdio reader is shielded
296
+ * by the same signal, so an abort terminates the line loop cleanly.
297
+ */
298
+ export async function serveStdio(options) {
299
+ const stdin = options.stdin ?? process.stdin;
300
+ const stdout = options.stdout ?? process.stdout;
301
+ const { server, signal } = options;
302
+ return new Promise((resolve) => {
303
+ let buffer = '';
304
+ let closed = false;
305
+ const finish = () => {
306
+ if (closed)
307
+ return;
308
+ closed = true;
309
+ stdin.off('data', onData);
310
+ stdin.off('end', finish);
311
+ stdin.off('close', finish);
312
+ if (signal) {
313
+ signal.removeEventListener('abort', finish);
314
+ }
315
+ resolve();
316
+ };
317
+ const writeFrame = (response) => {
318
+ stdout.write(`${JSON.stringify(response)}\n`);
319
+ };
320
+ const writeParseError = (id, message) => {
321
+ writeFrame({
322
+ jsonrpc: '2.0',
323
+ id,
324
+ error: { code: MCP_ERROR_CODES.PARSE_ERROR, message },
325
+ });
326
+ };
327
+ const processLine = async (line) => {
328
+ const trimmed = line.trim();
329
+ if (trimmed.length === 0)
330
+ return;
331
+ let parsed;
332
+ try {
333
+ parsed = JSON.parse(trimmed);
334
+ }
335
+ catch (error) {
336
+ writeParseError(null, `invalid JSON: ${error.message}`);
337
+ return;
338
+ }
339
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
340
+ writeParseError(null, 'request must be a JSON object');
341
+ return;
342
+ }
343
+ const candidate = parsed;
344
+ if (candidate.jsonrpc !== '2.0' || typeof candidate.method !== 'string') {
345
+ writeParseError(typeof candidate.id === 'number' || typeof candidate.id === 'string'
346
+ ? candidate.id
347
+ : null, 'invalid JSON-RPC envelope: jsonrpc=2.0 + string method required');
348
+ return;
349
+ }
350
+ // Stdio is a single-tenant transport — the parent process owns
351
+ // both ends — so we intentionally drop any inbound `meta.clientId`.
352
+ // Per-connection scoping only makes sense for HTTP+SSE.
353
+ const request = {
354
+ jsonrpc: '2.0',
355
+ method: candidate.method,
356
+ ...(candidate.id !== undefined ? { id: candidate.id } : {}),
357
+ ...(candidate.params !== undefined ? { params: candidate.params } : {}),
358
+ };
359
+ const response = await server.handleMessage(request);
360
+ if (response)
361
+ writeFrame(response);
362
+ };
363
+ const onData = (chunk) => {
364
+ buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
365
+ let newlineIndex = buffer.indexOf('\n');
366
+ while (newlineIndex !== -1) {
367
+ const line = buffer.slice(0, newlineIndex);
368
+ buffer = buffer.slice(newlineIndex + 1);
369
+ // Fire-and-forget the line handler; ordering of responses
370
+ // matches request ordering at the protocol level because the
371
+ // dispatch is microtask-serialized via the await above for
372
+ // each line — Node iterates `data` callbacks synchronously,
373
+ // and processLine awaits the dispatcher before returning so
374
+ // the next iteration of the loop sees the prior one settled.
375
+ void processLine(line);
376
+ newlineIndex = buffer.indexOf('\n');
377
+ }
378
+ };
379
+ if (signal) {
380
+ if (signal.aborted) {
381
+ finish();
382
+ return;
383
+ }
384
+ signal.addEventListener('abort', finish, { once: true });
385
+ }
386
+ if (!stdin.readable) {
387
+ // Stream already closed (e.g. parent piped EOF immediately).
388
+ finish();
389
+ return;
390
+ }
391
+ stdin.setEncoding('utf8');
392
+ stdin.on('data', onData);
393
+ stdin.on('end', finish);
394
+ stdin.on('close', finish);
395
+ });
396
+ }
397
+ //# sourceMappingURL=server.js.map