@vibecodetown/mcp-server 2.1.4 → 2.2.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.
Files changed (80) hide show
  1. package/README.md +10 -10
  2. package/build/auth/credential_store.js +146 -0
  3. package/build/auth/public_key.js +6 -4
  4. package/build/bootstrap/doctor.js +113 -5
  5. package/build/bootstrap/installer.js +85 -15
  6. package/build/bootstrap/registry.js +11 -6
  7. package/build/bootstrap/skills-installer.js +365 -0
  8. package/build/control_plane/gate.js +52 -70
  9. package/build/dx/activity.js +26 -3
  10. package/build/engine.js +151 -0
  11. package/build/errors.js +107 -0
  12. package/build/generated/bridge_build_seed_input.js +2 -0
  13. package/build/generated/bridge_build_seed_output.js +2 -0
  14. package/build/generated/bridge_confirm_reference_input.js +2 -0
  15. package/build/generated/bridge_confirm_reference_output.js +2 -0
  16. package/build/generated/bridge_confirmed_reference_file.js +2 -0
  17. package/build/generated/bridge_generate_references_input.js +2 -0
  18. package/build/generated/bridge_generate_references_output.js +2 -0
  19. package/build/generated/bridge_references_file.js +2 -0
  20. package/build/generated/bridge_work_order_seed_file.js +2 -0
  21. package/build/generated/contracts_bundle_info.js +3 -3
  22. package/build/generated/index.js +14 -0
  23. package/build/generated/ingress_input.js +2 -0
  24. package/build/generated/ingress_output.js +2 -0
  25. package/build/generated/ingress_resolution_file.js +2 -0
  26. package/build/generated/ingress_summary_file.js +2 -0
  27. package/build/generated/message_template_id_mapping_file.js +2 -0
  28. package/build/generated/run_app_input.js +1 -1
  29. package/build/index.js +4 -1
  30. package/build/local-mode/git.js +36 -22
  31. package/build/local-mode/paths.js +1 -0
  32. package/build/local-mode/project-state.js +176 -0
  33. package/build/local-mode/setup.js +21 -1
  34. package/build/local-mode/templates.js +3 -3
  35. package/build/path-utils.js +68 -0
  36. package/build/runtime/cli_invoker.js +416 -0
  37. package/build/tools/vibe_pm/advisory_review.js +5 -3
  38. package/build/tools/vibe_pm/bridge_build_seed.js +164 -0
  39. package/build/tools/vibe_pm/bridge_confirm_reference.js +91 -0
  40. package/build/tools/vibe_pm/bridge_generate_references.js +258 -0
  41. package/build/tools/vibe_pm/briefing.js +26 -1
  42. package/build/tools/vibe_pm/context.js +79 -0
  43. package/build/tools/vibe_pm/create_work_order.js +200 -3
  44. package/build/tools/vibe_pm/doctor.js +95 -0
  45. package/build/tools/vibe_pm/entity_gate/preflight.js +8 -3
  46. package/build/tools/vibe_pm/export_output.js +14 -13
  47. package/build/tools/vibe_pm/finalize_work.js +74 -0
  48. package/build/tools/vibe_pm/force_override.js +104 -0
  49. package/build/tools/vibe_pm/get_decision.js +2 -2
  50. package/build/tools/vibe_pm/index.js +160 -3
  51. package/build/tools/vibe_pm/ingress.js +645 -0
  52. package/build/tools/vibe_pm/ingress_gate.js +116 -0
  53. package/build/tools/vibe_pm/inspect_code.js +90 -20
  54. package/build/tools/vibe_pm/kce/doc_usage.js +4 -9
  55. package/build/tools/vibe_pm/kce/on_finalize.js +2 -2
  56. package/build/tools/vibe_pm/kce/preflight.js +11 -7
  57. package/build/tools/vibe_pm/list_rules.js +135 -0
  58. package/build/tools/vibe_pm/memory_status.js +11 -8
  59. package/build/tools/vibe_pm/memory_sync.js +11 -8
  60. package/build/tools/vibe_pm/pm_language.js +17 -16
  61. package/build/tools/vibe_pm/pre_commit_analysis.js +292 -0
  62. package/build/tools/vibe_pm/publish_mcp.js +271 -0
  63. package/build/tools/vibe_pm/python_error.js +115 -0
  64. package/build/tools/vibe_pm/run_app.js +215 -86
  65. package/build/tools/vibe_pm/run_app_podman.js +64 -2
  66. package/build/tools/vibe_pm/save_rule.js +120 -0
  67. package/build/tools/vibe_pm/search_oss.js +5 -3
  68. package/build/tools/vibe_pm/spec_rag.js +185 -0
  69. package/build/tools/vibe_pm/status.js +50 -3
  70. package/build/tools/vibe_pm/submit_decision.js +2 -2
  71. package/build/tools/vibe_pm/types.js +28 -0
  72. package/build/tools/vibe_pm/undo_last_task.js +23 -20
  73. package/build/tools/vibe_pm/waiter_mapping.js +155 -0
  74. package/build/tools/vibe_pm/zoekt_evidence.js +5 -3
  75. package/build/tools.js +13 -5
  76. package/build/version-check.js +5 -5
  77. package/build/vibe-cli.js +742 -39
  78. package/package.json +5 -4
  79. package/skills/VRIP_INSTALL_MANIFEST_DOCTOR.skill.md +288 -0
  80. package/skills/index.json +14 -0
@@ -0,0 +1,365 @@
1
+ // adapters/mcp-ts/src/bootstrap/skills-installer.ts
2
+ // Skill bundle installation, activation, and health check
3
+ // Skills are prebundled in the npm package (no network required)
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import crypto from "node:crypto";
7
+ import { SKILL_SPEC } from "./registry.js";
8
+ // ============================================================
9
+ // Path Utilities
10
+ // ============================================================
11
+ /**
12
+ * Find package root by looking for package.json
13
+ */
14
+ function findPackageRoot() {
15
+ // Start from current module's directory
16
+ let dir = path.dirname(new URL(import.meta.url).pathname);
17
+ // On Windows, remove leading slash from /C:/...
18
+ if (process.platform === "win32" && dir.startsWith("/")) {
19
+ dir = dir.slice(1);
20
+ }
21
+ for (let i = 0; i < 10; i++) {
22
+ const pkgPath = path.join(dir, "package.json");
23
+ try {
24
+ if (fs.existsSync(pkgPath)) {
25
+ return dir;
26
+ }
27
+ }
28
+ catch {
29
+ // ignore
30
+ }
31
+ const parent = path.dirname(dir);
32
+ if (parent === dir)
33
+ break;
34
+ dir = parent;
35
+ }
36
+ return null;
37
+ }
38
+ /**
39
+ * Get the path to the prebundled skills directory in the npm package
40
+ */
41
+ export function getSkillsBundlePath() {
42
+ const pkgRoot = findPackageRoot();
43
+ if (!pkgRoot)
44
+ return null;
45
+ const skillsDir = path.join(pkgRoot, SKILL_SPEC.bundlePath);
46
+ try {
47
+ if (fs.existsSync(skillsDir) && fs.statSync(skillsDir).isDirectory()) {
48
+ return skillsDir;
49
+ }
50
+ }
51
+ catch {
52
+ // ignore
53
+ }
54
+ return null;
55
+ }
56
+ /**
57
+ * Get the path to the project's .vibe/skills/ directory
58
+ */
59
+ export function getProjectSkillsPath(projectRoot) {
60
+ const root = projectRoot || process.cwd();
61
+ return path.join(root, ".vibe", "skills");
62
+ }
63
+ // ============================================================
64
+ // Index Loading
65
+ // ============================================================
66
+ /**
67
+ * Load the skills index from the prebundled directory
68
+ */
69
+ export function loadSkillsIndex() {
70
+ const bundlePath = getSkillsBundlePath();
71
+ if (!bundlePath)
72
+ return null;
73
+ const indexPath = path.join(bundlePath, "index.json");
74
+ try {
75
+ const content = fs.readFileSync(indexPath, "utf-8");
76
+ return JSON.parse(content);
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Load the activated skills index from the project
84
+ */
85
+ export function loadActivatedSkillsIndex(projectRoot) {
86
+ const skillsPath = getProjectSkillsPath(projectRoot);
87
+ const indexPath = path.join(skillsPath, "index.json");
88
+ try {
89
+ const content = fs.readFileSync(indexPath, "utf-8");
90
+ return JSON.parse(content);
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ // ============================================================
97
+ // Health Check
98
+ // ============================================================
99
+ /**
100
+ * Calculate SHA256 hash of a file
101
+ */
102
+ function calculateSha256(filePath) {
103
+ try {
104
+ const content = fs.readFileSync(filePath);
105
+ return crypto.createHash("sha256").update(content).digest("hex");
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ /**
112
+ * Check health of a single skill in the bundle
113
+ */
114
+ function checkSkillHealth(bundlePath, skill) {
115
+ const filePath = path.join(bundlePath, skill.file);
116
+ if (!fs.existsSync(filePath)) {
117
+ return {
118
+ id: skill.id,
119
+ status: "missing",
120
+ version: skill.version,
121
+ path: null,
122
+ error: `Skill file not found: ${skill.file}`
123
+ };
124
+ }
125
+ const actualSha = calculateSha256(filePath);
126
+ if (!actualSha) {
127
+ return {
128
+ id: skill.id,
129
+ status: "sha_mismatch",
130
+ version: skill.version,
131
+ path: filePath,
132
+ error: "Failed to calculate SHA256"
133
+ };
134
+ }
135
+ if (actualSha !== skill.sha256) {
136
+ return {
137
+ id: skill.id,
138
+ status: "sha_mismatch",
139
+ version: skill.version,
140
+ path: filePath,
141
+ error: `SHA256 mismatch: expected ${skill.sha256}, got ${actualSha}`
142
+ };
143
+ }
144
+ return {
145
+ id: skill.id,
146
+ status: "ok",
147
+ version: skill.version,
148
+ path: filePath
149
+ };
150
+ }
151
+ /**
152
+ * Check health of the entire skills bundle
153
+ */
154
+ export function getSkillsHealth() {
155
+ const bundlePath = getSkillsBundlePath();
156
+ if (!bundlePath) {
157
+ return {
158
+ installed: false,
159
+ bundlePath: null,
160
+ version: null,
161
+ skills: [],
162
+ summary: { total: 0, ok: 0, missing: 0, corrupted: 0 }
163
+ };
164
+ }
165
+ const index = loadSkillsIndex();
166
+ if (!index) {
167
+ return {
168
+ installed: true,
169
+ bundlePath,
170
+ version: null,
171
+ skills: [],
172
+ summary: { total: 0, ok: 0, missing: 0, corrupted: 0 }
173
+ };
174
+ }
175
+ const skills = index.skills.map(skill => checkSkillHealth(bundlePath, skill));
176
+ const summary = {
177
+ total: skills.length,
178
+ ok: skills.filter(s => s.status === "ok").length,
179
+ missing: skills.filter(s => s.status === "missing").length,
180
+ corrupted: skills.filter(s => s.status === "sha_mismatch").length
181
+ };
182
+ return {
183
+ installed: true,
184
+ bundlePath,
185
+ version: index.version,
186
+ skills,
187
+ summary
188
+ };
189
+ }
190
+ // ============================================================
191
+ // Skill Listing
192
+ // ============================================================
193
+ /**
194
+ * List all available skills from the prebundled directory
195
+ */
196
+ export function listAvailableSkills() {
197
+ const index = loadSkillsIndex();
198
+ return index?.skills ?? [];
199
+ }
200
+ /**
201
+ * List activated skills in the project
202
+ */
203
+ export function listActivatedSkills(projectRoot) {
204
+ const index = loadActivatedSkillsIndex(projectRoot);
205
+ return index?.activated ?? [];
206
+ }
207
+ /**
208
+ * Get skill metadata by ID
209
+ */
210
+ export function getSkillById(skillId) {
211
+ const skills = listAvailableSkills();
212
+ return skills.find(s => s.id === skillId) ?? null;
213
+ }
214
+ // ============================================================
215
+ // Skill Activation
216
+ // ============================================================
217
+ /**
218
+ * Activate skills for a project by copying them to .vibe/skills/
219
+ */
220
+ export async function activateSkills(projectRoot, skillIds) {
221
+ const bundlePath = getSkillsBundlePath();
222
+ if (!bundlePath) {
223
+ return { activated: [], errors: ["Skill bundle not found in package"] };
224
+ }
225
+ const index = loadSkillsIndex();
226
+ if (!index) {
227
+ return { activated: [], errors: ["Skills index not found or invalid"] };
228
+ }
229
+ // If no skillIds specified, activate all
230
+ const toActivate = skillIds ?? index.skills.map(s => s.id);
231
+ const skillsToActivate = toActivate
232
+ .map(id => index.skills.find(s => s.id === id))
233
+ .filter((s) => s !== undefined);
234
+ if (skillsToActivate.length === 0) {
235
+ return { activated: [], errors: ["No valid skills to activate"] };
236
+ }
237
+ const projectSkillsPath = getProjectSkillsPath(projectRoot);
238
+ // Ensure .vibe/skills/ directory exists
239
+ try {
240
+ await fs.promises.mkdir(projectSkillsPath, { recursive: true });
241
+ }
242
+ catch (e) {
243
+ return {
244
+ activated: [],
245
+ errors: [`Failed to create skills directory: ${e instanceof Error ? e.message : String(e)}`]
246
+ };
247
+ }
248
+ const activated = [];
249
+ const errors = [];
250
+ // Copy each skill file
251
+ for (const skill of skillsToActivate) {
252
+ const srcPath = path.join(bundlePath, skill.file);
253
+ const destPath = path.join(projectSkillsPath, skill.file);
254
+ try {
255
+ await fs.promises.copyFile(srcPath, destPath);
256
+ activated.push(skill.id);
257
+ }
258
+ catch (e) {
259
+ errors.push(`Failed to copy ${skill.id}: ${e instanceof Error ? e.message : String(e)}`);
260
+ }
261
+ }
262
+ // Write the activated skills index
263
+ if (activated.length > 0) {
264
+ const activatedIndex = {
265
+ version: index.version,
266
+ activated,
267
+ activated_at: new Date().toISOString(),
268
+ source: "prebundled",
269
+ skills: skillsToActivate.filter(s => activated.includes(s.id))
270
+ };
271
+ const indexPath = path.join(projectSkillsPath, "index.json");
272
+ try {
273
+ await fs.promises.writeFile(indexPath, JSON.stringify(activatedIndex, null, 2), "utf-8");
274
+ }
275
+ catch (e) {
276
+ errors.push(`Failed to write index.json: ${e instanceof Error ? e.message : String(e)}`);
277
+ }
278
+ }
279
+ return { activated, errors };
280
+ }
281
+ /**
282
+ * Deactivate a skill from the project
283
+ */
284
+ export async function deactivateSkill(projectRoot, skillId) {
285
+ const projectSkillsPath = getProjectSkillsPath(projectRoot);
286
+ const activatedIndex = loadActivatedSkillsIndex(projectRoot);
287
+ if (!activatedIndex) {
288
+ return { success: false, error: "No activated skills found" };
289
+ }
290
+ const skillMeta = activatedIndex.skills.find(s => s.id === skillId);
291
+ if (!skillMeta) {
292
+ return { success: false, error: `Skill ${skillId} is not activated` };
293
+ }
294
+ // Remove the skill file
295
+ const skillPath = path.join(projectSkillsPath, skillMeta.file);
296
+ try {
297
+ if (fs.existsSync(skillPath)) {
298
+ await fs.promises.unlink(skillPath);
299
+ }
300
+ }
301
+ catch (e) {
302
+ return {
303
+ success: false,
304
+ error: `Failed to remove skill file: ${e instanceof Error ? e.message : String(e)}`
305
+ };
306
+ }
307
+ // Update the index
308
+ const newActivated = activatedIndex.activated.filter(id => id !== skillId);
309
+ const newSkills = activatedIndex.skills.filter(s => s.id !== skillId);
310
+ if (newActivated.length === 0) {
311
+ // Remove the index file if no skills remain
312
+ const indexPath = path.join(projectSkillsPath, "index.json");
313
+ try {
314
+ await fs.promises.unlink(indexPath);
315
+ }
316
+ catch {
317
+ // ignore
318
+ }
319
+ }
320
+ else {
321
+ // Update the index
322
+ const updatedIndex = {
323
+ ...activatedIndex,
324
+ activated: newActivated,
325
+ skills: newSkills
326
+ };
327
+ const indexPath = path.join(projectSkillsPath, "index.json");
328
+ await fs.promises.writeFile(indexPath, JSON.stringify(updatedIndex, null, 2), "utf-8");
329
+ }
330
+ return { success: true };
331
+ }
332
+ // ============================================================
333
+ // Ensure Skills (for setup/update)
334
+ // ============================================================
335
+ /**
336
+ * Ensure skills are installed and healthy
337
+ * Returns the bundle path if successful
338
+ */
339
+ export function ensureSkills() {
340
+ const health = getSkillsHealth();
341
+ // For prebundled skills, we can't "repair" - they're part of the package
342
+ // If they're corrupted, the user needs to reinstall the package
343
+ return {
344
+ path: health.bundlePath,
345
+ health
346
+ };
347
+ }
348
+ /**
349
+ * Get skill content by ID
350
+ */
351
+ export function getSkillContent(skillId) {
352
+ const bundlePath = getSkillsBundlePath();
353
+ if (!bundlePath)
354
+ return null;
355
+ const skill = getSkillById(skillId);
356
+ if (!skill)
357
+ return null;
358
+ const filePath = path.join(bundlePath, skill.file);
359
+ try {
360
+ return fs.readFileSync(filePath, "utf-8");
361
+ }
362
+ catch {
363
+ return null;
364
+ }
365
+ }
@@ -3,8 +3,10 @@
3
3
  *
4
4
  * Provides gate enforcement for work orders before execution.
5
5
  * All work orders must pass through the gate before any work can proceed.
6
+ *
7
+ * All subprocess invocations go through cli_invoker for centralized management.
6
8
  */
7
- import { spawn } from "child_process";
9
+ import { invokeSystem } from "../runtime/cli_invoker.js";
8
10
  import { WorkOrderV1Schema } from "../generated/work_order_v1.js";
9
11
  import { GateResultV1Schema } from "../generated/gate_result_v1.js";
10
12
  /**
@@ -30,76 +32,56 @@ export async function runSystemDesignGate(workOrder, options) {
30
32
  pushBool("fail-fast", options.failFast);
31
33
  pushBool("semgrep", options.semgrep);
32
34
  pushBool("runtime", options.runtime);
33
- return new Promise((resolve) => {
34
- const child = spawn(pythonExe, [
35
- "-m",
36
- "vibecoding_helper",
37
- "--repo-root",
38
- repoRoot,
39
- "gate-check",
40
- ...overrideFlags,
41
- "--work-order",
42
- workOrderJson,
43
- "--format",
44
- "json",
45
- ], {
46
- cwd: repoRoot,
47
- timeout: timeoutMs,
48
- env: { ...process.env, PYTHONIOENCODING: "utf-8" },
49
- });
50
- let stdout = "";
51
- let stderr = "";
52
- child.stdout?.on("data", (data) => {
53
- stdout += data.toString();
54
- });
55
- child.stderr?.on("data", (data) => {
56
- stderr += data.toString();
57
- });
58
- child.on("close", (code) => {
59
- // Exit code 0 = ALLOW, 2 = BLOCK, other = error
60
- if (code === null) {
61
- resolve({
62
- success: false,
63
- error: "Gate check timed out",
64
- });
65
- return;
66
- }
67
- if (code !== 0 && code !== 2) {
68
- resolve({
69
- success: false,
70
- error: `Gate check failed with code ${code}: ${stderr || stdout}`,
71
- });
72
- return;
73
- }
74
- try {
75
- const result = JSON.parse(stdout.trim());
76
- const validated = GateResultV1Schema.safeParse(result);
77
- if (!validated.success) {
78
- resolve({
79
- success: false,
80
- error: `Invalid gate result: ${validated.error.message}`,
81
- });
82
- return;
83
- }
84
- resolve({
85
- success: true,
86
- result: validated.data,
87
- });
88
- }
89
- catch (e) {
90
- resolve({
91
- success: false,
92
- error: `Failed to parse gate result: ${e instanceof Error ? e.message : String(e)}`,
93
- });
94
- }
95
- });
96
- child.on("error", (err) => {
97
- resolve({
98
- success: false,
99
- error: `Gate check process error: ${err.message}`,
100
- });
101
- });
35
+ const args = [
36
+ "-m",
37
+ "vibecoding_helper",
38
+ "--repo-root",
39
+ repoRoot,
40
+ "gate-check",
41
+ ...overrideFlags,
42
+ "--work-order",
43
+ workOrderJson,
44
+ "--format",
45
+ "json",
46
+ ];
47
+ const result = await invokeSystem(pythonExe, args, repoRoot, {
48
+ env: { PYTHONIOENCODING: "utf-8" },
49
+ timeoutMs,
102
50
  });
51
+ // Exit code 124 = timeout
52
+ if (result.exitCode === 124) {
53
+ return {
54
+ success: false,
55
+ error: "Gate check timed out",
56
+ };
57
+ }
58
+ // Exit code 0 = ALLOW, 2 = BLOCK, other = error
59
+ if (result.exitCode !== 0 && result.exitCode !== 2) {
60
+ return {
61
+ success: false,
62
+ error: `Gate check failed with code ${result.exitCode}: ${result.stderr || result.stdout}`,
63
+ };
64
+ }
65
+ try {
66
+ const parsed = JSON.parse(result.stdout.trim());
67
+ const validated = GateResultV1Schema.safeParse(parsed);
68
+ if (!validated.success) {
69
+ return {
70
+ success: false,
71
+ error: `Invalid gate result: ${validated.error.message}`,
72
+ };
73
+ }
74
+ return {
75
+ success: true,
76
+ result: validated.data,
77
+ };
78
+ }
79
+ catch (e) {
80
+ return {
81
+ success: false,
82
+ error: `Failed to parse gate result: ${e instanceof Error ? e.message : String(e)}`,
83
+ };
84
+ }
103
85
  }
104
86
  /**
105
87
  * Execute a work order only if it passes the gate
@@ -1,5 +1,6 @@
1
1
  // adapters/mcp-ts/src/dx/activity.ts
2
2
  // Activity/DX helpers: branded header, status icons, optional desktop notifications
3
+ import { getWaiterMapping, renderVerdictFromTemplateId, resolveLocaleFromData } from "../tools/vibe_pm/waiter_mapping.js";
3
4
  function boolEnv(name, defaultValue) {
4
5
  const raw = (process.env[name] ?? "").trim().toLowerCase();
5
6
  if (!raw)
@@ -91,14 +92,35 @@ function verdictIcon(data, outcome) {
91
92
  }
92
93
  return "✅";
93
94
  }
95
+ function verdictLabel(data, outcome) {
96
+ if (outcome === "error")
97
+ return "";
98
+ if (!data || typeof data !== "object")
99
+ return "";
100
+ const obj = data;
101
+ const templateId = typeof obj.message_template_id === "string" ? String(obj.message_template_id) : "";
102
+ if (!templateId)
103
+ return "";
104
+ try {
105
+ const mapping = getWaiterMapping();
106
+ const locale = resolveLocaleFromData(data, mapping);
107
+ const label = renderVerdictFromTemplateId(templateId, locale, mapping);
108
+ return label ? label : "";
109
+ }
110
+ catch {
111
+ return "";
112
+ }
113
+ }
94
114
  export function renderActivityHeader(toolName, data, outcome = "success", durationMs) {
95
115
  const badge = toolBadge(toolName, data);
96
116
  const status = verdictIcon(data, outcome);
117
+ const label = verdictLabel(data, outcome);
97
118
  const duration = typeof durationMs === "number" && durationMs >= 0 ? ` · ${Math.round(durationMs)}ms` : "";
119
+ const verdict = label ? ` · ${label}` : "";
98
120
  return [
99
121
  "╭──────────────────────────────╮",
100
122
  "│ VibePM ⚡️ Active │",
101
- `│ ${status} ${badge.icon} ${badge.label}${duration}`,
123
+ `│ ${status} ${badge.icon} ${badge.label}${verdict}${duration}`,
102
124
  "╰──────────────────────────────╯"
103
125
  ].join("\n");
104
126
  }
@@ -107,9 +129,10 @@ export function decorateToolOutputText(toolName, jsonText, data, outcome = "succ
107
129
  return [{ type: "text", text: jsonText }];
108
130
  }
109
131
  const header = renderActivityHeader(toolName, data, outcome, durationMs);
132
+ // Combine header and JSON into single block to ensure both are displayed
133
+ // (some MCP clients only show the first content block on error)
110
134
  return [
111
- { type: "text", text: header },
112
- { type: "text", text: jsonText }
135
+ { type: "text", text: header + "\n" + jsonText }
113
136
  ];
114
137
  }
115
138
  export async function notifyDesktopBestEffort(args) {