@sage-protocol/cli 0.2.9 → 0.3.2

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 (42) hide show
  1. package/dist/cli/commands/config.js +28 -0
  2. package/dist/cli/commands/doctor.js +87 -8
  3. package/dist/cli/commands/gov-config.js +81 -0
  4. package/dist/cli/commands/governance.js +152 -72
  5. package/dist/cli/commands/library.js +9 -0
  6. package/dist/cli/commands/proposals.js +187 -17
  7. package/dist/cli/commands/skills.js +737 -0
  8. package/dist/cli/commands/subdao.js +96 -132
  9. package/dist/cli/config/playbooks.json +62 -0
  10. package/dist/cli/config.js +15 -0
  11. package/dist/cli/governance-manager.js +25 -4
  12. package/dist/cli/index.js +6 -7
  13. package/dist/cli/library-manager.js +79 -0
  14. package/dist/cli/mcp-server-stdio.js +1387 -166
  15. package/dist/cli/schemas/manifest.schema.json +55 -0
  16. package/dist/cli/services/doctor/fixers.js +134 -0
  17. package/dist/cli/services/governance/doctor.js +140 -0
  18. package/dist/cli/services/governance/playbooks.js +97 -0
  19. package/dist/cli/services/mcp/bulk-operations.js +272 -0
  20. package/dist/cli/services/mcp/dependency-analyzer.js +202 -0
  21. package/dist/cli/services/mcp/library-listing.js +2 -2
  22. package/dist/cli/services/mcp/local-prompt-collector.js +1 -0
  23. package/dist/cli/services/mcp/manifest-downloader.js +5 -3
  24. package/dist/cli/services/mcp/manifest-fetcher.js +17 -1
  25. package/dist/cli/services/mcp/manifest-workflows.js +127 -15
  26. package/dist/cli/services/mcp/quick-start.js +287 -0
  27. package/dist/cli/services/mcp/stdio-runner.js +30 -5
  28. package/dist/cli/services/mcp/template-manager.js +156 -0
  29. package/dist/cli/services/mcp/templates/default-templates.json +84 -0
  30. package/dist/cli/services/mcp/tool-args-validator.js +56 -0
  31. package/dist/cli/services/mcp/trending-formatter.js +1 -1
  32. package/dist/cli/services/mcp/unified-prompt-search.js +2 -2
  33. package/dist/cli/services/metaprompt/designer.js +12 -5
  34. package/dist/cli/services/skills/discovery.js +99 -0
  35. package/dist/cli/services/subdao/applier.js +229 -0
  36. package/dist/cli/services/subdao/planner.js +142 -0
  37. package/dist/cli/subdao-manager.js +14 -0
  38. package/dist/cli/utils/aliases.js +28 -6
  39. package/dist/cli/utils/contract-error-decoder.js +61 -0
  40. package/dist/cli/utils/suggestions.js +25 -13
  41. package/package.json +3 -2
  42. package/src/schemas/manifest.schema.json +55 -0
@@ -23,6 +23,12 @@ const { resolveDiscoverySettings } = require('./services/ipfs/discovery-config')
23
23
  process.env.MCP_MODE = 'true';
24
24
  process.env.IPFS_SILENT = 'true';
25
25
 
26
+ // CRITICAL: Redirect console.log to stderr to prevent libraries from polluting stdout
27
+ // JSON-RPC responses must be the ONLY thing written to stdout.
28
+ const originalConsoleLog = console.log;
29
+ console.log = console.error;
30
+
31
+
26
32
  // Load environment variables without dotenv promotional messages
27
33
  const LibraryManager = require('./library-manager');
28
34
  const metapromptDesigner = require('./utils/metaprompt-designer');
@@ -63,19 +69,147 @@ class SageMCPServer {
63
69
  name: 'sage-protocol-mcp',
64
70
  version: '1.0.0'
65
71
  };
66
-
72
+
67
73
  this.discoverySettings = resolveDiscoverySettings();
68
74
  this.discoveryClient = undefined;
69
-
75
+
70
76
  this.capabilities = {
71
77
  tools: {},
72
78
  prompts: {}
73
79
  };
74
80
 
75
- this.tools = [
81
+ this.tools = [
82
+ {
83
+ name: 'quick_create_prompt',
84
+ description: `Quickly create a new prompt. Handles library creation automatically.
85
+ When to use:
86
+ - Starting a new prompt from scratch
87
+ - Creating a prompt in "My Library" (default)
88
+
89
+ Prerequisites:
90
+ - None! Just need a name and content.
91
+
92
+ Next steps:
93
+ - quick_test_prompt() to verify
94
+ - quick_iterate_prompt() to refine`,
95
+ inputSchema: {
96
+ type: 'object',
97
+ properties: {
98
+ name: { type: 'string', description: 'Name of the prompt' },
99
+ content: { type: 'string', description: 'Prompt content (supports ${variable} syntax)' },
100
+ description: { type: 'string', description: 'Optional description' },
101
+ library: { type: 'string', description: 'Optional library name (defaults to "My Library")' }
102
+ },
103
+ required: ['name', 'content']
104
+ }
105
+ },
106
+ {
107
+ name: 'quick_iterate_prompt',
108
+ description: `Update an existing prompt. Creates a backup of the previous version.
109
+ When to use:
110
+ - Refining prompt content
111
+ - Renaming a prompt
112
+ - Safe editing (auto-backup)
113
+
114
+ Prerequisites:
115
+ - Key of the prompt (from list_prompts)
116
+
117
+ Next steps:
118
+ - quick_test_prompt() to verify changes`,
119
+ inputSchema: {
120
+ type: 'object',
121
+ properties: {
122
+ key: { type: 'string', description: 'Key of the prompt to update' },
123
+ content: { type: 'string', description: 'New content' },
124
+ name: { type: 'string', description: 'New name' }
125
+ },
126
+ required: ['key']
127
+ }
128
+ },
129
+ {
130
+ name: 'improve_prompt',
131
+ description: `Analyze an existing prompt and suggest improvement areas and interview questions.
132
+ When to use:
133
+ - You have a working prompt and want to refine it
134
+ - You want structured feedback and question prompts for the user
135
+
136
+ Prerequisites:
137
+ - Key of the prompt`,
138
+ inputSchema: {
139
+ type: 'object',
140
+ properties: {
141
+ key: { type: 'string', description: 'Prompt key' },
142
+ library: { type: 'string', description: 'Optional library name or CID to narrow search' },
143
+ focus: { type: 'string', description: 'Optional focus area (e.g. "clarity", "variables", "edge-cases")' }
144
+ },
145
+ required: ['key']
146
+ }
147
+ },
148
+ {
149
+ name: 'rename_prompt',
150
+ description: `Rename a prompt's key and/or display name.
151
+ When to use:
152
+ - You want a cleaner key or display name
153
+ - You want to align keys across related prompts
154
+
155
+ Notes:
156
+ - Keys are stable identifiers; renaming a key will affect how you reference the prompt in tools.`,
157
+ inputSchema: {
158
+ type: 'object',
159
+ properties: {
160
+ key: { type: 'string', description: 'Existing prompt key' },
161
+ newKey: { type: 'string', description: 'New key (optional, stable identifier)' },
162
+ name: { type: 'string', description: 'New display name (optional)' }
163
+ },
164
+ required: ['key']
165
+ }
166
+ },
167
+ {
168
+ name: 'quick_test_prompt',
169
+ description: `Alias for test_prompt. Test a prompt with variables.
170
+ When to use:
171
+ - Verifying prompt behavior
172
+ - Checking variable substitution
173
+ - Debugging prompt logic
174
+
175
+ Prerequisites:
176
+ - Key of the prompt
177
+
178
+ Next steps:
179
+ - quick_iterate_prompt() if changes needed`,
180
+ inputSchema: {
181
+ type: 'object',
182
+ properties: {
183
+ key: { type: 'string', description: 'Prompt key' },
184
+ variables: { type: 'object', description: 'Variables for substitution' }
185
+ },
186
+ required: ['key']
187
+ }
188
+ },
189
+ {
190
+ name: 'help',
191
+ description: 'Get help on how to use Sage MCP tools and workflows',
192
+ inputSchema: {
193
+ type: 'object',
194
+ properties: {
195
+ topic: { type: 'string', description: 'Specific topic (e.g., "create", "publish", "versioning")' }
196
+ }
197
+ }
198
+ },
199
+
76
200
  {
77
201
  name: 'search_prompts',
78
- description: 'Unified prompt search across local pinned libraries and on-chain registries',
202
+ description: `Unified prompt search across local pinned libraries and on-chain registries.
203
+ When to use:
204
+ - Finding prompts by topic, tag, or content
205
+ - Searching across BOTH local and on-chain sources
206
+
207
+ Prerequisites:
208
+ - None
209
+
210
+ Next steps:
211
+ - get_prompt(key) to view details
212
+ - quick_test_prompt(key) to try it`,
79
213
  inputSchema: {
80
214
  type: 'object',
81
215
  properties: {
@@ -102,6 +236,77 @@ class SageMCPServer {
102
236
  required: []
103
237
  }
104
238
  },
239
+ {
240
+ name: 'list_prompts',
241
+ description: `List all available prompts from local libraries.
242
+ When to use:
243
+ - Browsing your local collection
244
+ - Seeing what's available in a specific library
245
+
246
+ Prerequisites:
247
+ - None
248
+
249
+ Next steps:
250
+ - get_prompt(key) to view details`,
251
+ inputSchema: {
252
+ type: 'object',
253
+ properties: {
254
+ source: { type: 'string', enum: ['local', 'all'], description: 'Data source (default: local)' },
255
+ library: { type: 'string', description: 'Optional: filter by library name or CID' },
256
+ limit: { type: 'number', description: 'Max prompts to return (default: 20)' }
257
+ },
258
+ required: []
259
+ }
260
+ },
261
+ {
262
+ name: 'get_prompt',
263
+ description: `Get a specific prompt by key, including its full content.
264
+ When to use:
265
+ - Viewing/editing a known prompt
266
+ - Testing prompt with variables
267
+ - Retrieving prompt for copying/forking
268
+
269
+ Prerequisites:
270
+ - Know the prompt key (from list_prompts or search_prompts)
271
+
272
+ Next steps:
273
+ - quick_test_prompt() to fill variables
274
+ - quick_iterate_prompt() to edit`,
275
+ inputSchema: {
276
+ type: 'object',
277
+ properties: {
278
+ key: { type: 'string', description: 'Prompt key (e.g. "daily-reflection")' },
279
+ library: { type: 'string', description: 'Optional: library name or CID to narrow search' }
280
+ },
281
+ required: ['key']
282
+ }
283
+ },
284
+ {
285
+ name: 'test_prompt',
286
+ description: `Test a prompt by filling in \${variable} placeholders and seeing the rendered result.
287
+ When to use:
288
+ - Verifying prompt behavior
289
+ - Checking variable substitution
290
+ - Debugging prompt logic
291
+
292
+ Prerequisites:
293
+ - Key of the prompt
294
+
295
+ Next steps:
296
+ - quick_iterate_prompt() if changes needed`,
297
+ inputSchema: {
298
+ type: 'object',
299
+ properties: {
300
+ key: { type: 'string', description: 'Prompt key' },
301
+ library: { type: 'string', description: 'Optional: library name or CID' },
302
+ variables: {
303
+ type: 'object',
304
+ description: 'Key-value pairs for ${variable} placeholders (e.g. {"language": "Python"})'
305
+ }
306
+ },
307
+ required: ['key']
308
+ }
309
+ },
105
310
  {
106
311
  name: 'search_onchain_prompts',
107
312
  description: 'Search for prompts directly on-chain from LibraryRegistry and SubDAO registries',
@@ -163,7 +368,8 @@ class SageMCPServer {
163
368
  properties: {
164
369
  goal: { type: 'string', description: 'High-level objective for the resulting system prompt' },
165
370
  model: { type: 'string', description: 'Target model identifier (default: gpt-5)' },
166
- interviewStyle: { type: 'string', description: 'Cadence preference (default: one-question-at-a-time)' }
371
+ interviewStyle: { type: 'string', description: 'Cadence preference (default: one-question-at-a-time)' },
372
+ additionalInstructions: { type: 'string', description: 'Any extra instructions for the interviewer (e.g. "Be concise", "Focus on security")' }
167
373
  },
168
374
  required: []
169
375
  }
@@ -307,22 +513,23 @@ class SageMCPServer {
307
513
  },
308
514
  {
309
515
  name: 'propose_manifest',
310
- description: 'Build governance payload for a manifest CID and suggest CLI commands',
516
+ description: `Generate the proposal payload and CLI commands for a SubDAO update.
517
+ Note: This tool does NOT sign transactions. It generates the hex data and CLI commands for you to execute in your terminal.`,
311
518
  inputSchema: {
312
519
  type: 'object',
313
520
  properties: {
314
- cid: { type: 'string', description: 'Manifest CID' },
315
- subdao: { type: 'string', description: 'Target SubDAO address (optional if governor context is persisted)' },
316
- description: { type: 'string', description: 'Proposal description override (optional)' },
317
- promptCount: { type: 'number', description: 'Prompt count if known (optional)' },
318
- manifest: { type: 'object', description: 'Manifest JSON (optional, used for prompt count/previous)' }
521
+ cid: { type: 'string', description: 'IPFS CID of the manifest' },
522
+ subdao: { type: 'string', description: 'SubDAO address' },
523
+ description: { type: 'string', description: 'Proposal description' },
524
+ promptCount: { type: 'number', description: 'Number of prompts (optional override)' },
525
+ manifest: { type: 'object', description: 'Optional manifest object for context' }
319
526
  },
320
- required: ['cid']
527
+ required: ['cid', 'subdao']
321
528
  }
322
529
  },
323
530
  {
324
531
  name: 'pin_library',
325
- description: 'Save a manifest JSON to local cache (~/.sage/libraries/<cid>.json)',
532
+ description: 'Pin a library manifest to local IPFS node (simulated or real)',
326
533
  inputSchema: {
327
534
  type: 'object',
328
535
  properties: {
@@ -333,35 +540,35 @@ class SageMCPServer {
333
540
  },
334
541
  {
335
542
  name: 'pin_by_cid',
336
- description: 'Fetch a manifest by CID from IPFS and save it to local cache',
543
+ description: 'Fetch a manifest by CID and pin it locally',
337
544
  inputSchema: {
338
545
  type: 'object',
339
546
  properties: {
340
- cid: { type: 'string', description: 'Manifest CID' }
547
+ cid: { type: 'string', description: 'CID to fetch and pin' }
341
548
  },
342
549
  required: ['cid']
343
550
  }
344
551
  },
345
552
  {
346
553
  name: 'diff_manifests',
347
- description: 'Diff a previous manifest CID (IPFS) against a new manifest JSON',
554
+ description: 'Compare two manifests (previous CID vs new manifest object)',
348
555
  inputSchema: {
349
556
  type: 'object',
350
557
  properties: {
351
- previousCid: { type: 'string', description: 'Previous manifest CID' },
352
- nextManifest: { type: 'object', description: 'Next manifest JSON' }
558
+ previousCid: { type: 'string', description: 'Base CID' },
559
+ nextManifest: { type: 'object', description: 'New manifest object' }
353
560
  },
354
561
  required: ['previousCid', 'nextManifest']
355
562
  }
356
563
  },
357
564
  {
358
565
  name: 'list_proposals',
359
- description: 'List proposals (subgraph-first) with friendly summaries',
566
+ description: 'List active proposals for a SubDAO',
360
567
  inputSchema: {
361
568
  type: 'object',
362
569
  properties: {
363
- subdao: { type: 'string', description: 'Optional SubDAO address to scope results' },
364
- state: { type: 'string', description: 'Optional state filter (Pending|Active|Succeeded|Queued|Executed...)' },
570
+ subdao: { type: 'string', description: 'SubDAO address' },
571
+ state: { type: 'string', description: 'Filter by state (Active, Executed, etc)' },
365
572
  limit: { type: 'number', description: 'Max items (default 10)' }
366
573
  },
367
574
  required: []
@@ -369,17 +576,190 @@ class SageMCPServer {
369
576
  },
370
577
  {
371
578
  name: 'publish_manifest_flow',
372
- description: 'Validate push build proposal payload with CLI hints (no signing)',
579
+ description: `Prepare a manifest for on-chain publishing.
580
+ 1. Validates the manifest
581
+ 2. Pins it to IPFS
582
+ 3. Generates CLI commands for you to sign & broadcast
583
+
584
+ Note: This tool does NOT sign transactions. It prepares everything so you can execute the final step in your terminal.`,
373
585
  inputSchema: {
374
586
  type: 'object',
375
587
  properties: {
376
588
  manifest: { type: 'object', description: 'Manifest JSON' },
377
589
  subdao: { type: 'string', description: 'Optional SubDAO address for hints' },
378
- description: { type: 'string', description: 'Optional description override' }
590
+ description: { type: 'string', description: 'Optional description override' },
591
+ dry_run: { type: 'boolean', description: 'Validate and build proposal payload without uploading to IPFS (no network calls)' }
379
592
  },
380
593
  required: ['manifest']
381
594
  }
382
595
  },
596
+ {
597
+ name: 'update_library_metadata',
598
+ description: 'Update library-level metadata (name/description/tags) and optionally propagate tags to prompts.',
599
+ inputSchema: {
600
+ type: 'object',
601
+ properties: {
602
+ library: {
603
+ type: 'string',
604
+ description: 'Pinned library name or CID (case-insensitive name match)'
605
+ },
606
+ name: { type: 'string', description: 'New library name (optional)' },
607
+ description: { type: 'string', description: 'New library description (optional)' },
608
+ tags: {
609
+ type: 'array',
610
+ items: { type: 'string' },
611
+ description: 'Library-level tags (replaces existing)'
612
+ },
613
+ apply_to_prompts: {
614
+ type: 'boolean',
615
+ description: 'If true and tags provided, propagate tags to prompts using merge_mode',
616
+ default: false
617
+ },
618
+ merge_mode: {
619
+ type: 'string',
620
+ enum: ['replace', 'merge'],
621
+ description: 'Prompt tag strategy when apply_to_prompts=true',
622
+ default: 'merge'
623
+ }
624
+ },
625
+ required: ['library']
626
+ }
627
+ },
628
+ {
629
+ name: 'bulk_update_prompts',
630
+ description: 'Apply multiple small updates (name/description/tags/content) to prompts in a single library.',
631
+ inputSchema: {
632
+ type: 'object',
633
+ properties: {
634
+ library: {
635
+ type: 'string',
636
+ description: 'Pinned library name or CID'
637
+ },
638
+ updates: {
639
+ type: 'array',
640
+ items: {
641
+ type: 'object',
642
+ properties: {
643
+ key: {
644
+ type: 'string',
645
+ description: 'Prompt key within the manifest'
646
+ },
647
+ name: { type: 'string', description: 'New prompt display name (optional)' },
648
+ description: { type: 'string', description: 'New prompt description (optional)' },
649
+ tags: {
650
+ type: 'array',
651
+ items: { type: 'string' },
652
+ description: 'If provided, replaces the prompt tags'
653
+ },
654
+ content: {
655
+ type: 'string',
656
+ description: 'New prompt content body (optional)'
657
+ }
658
+ },
659
+ required: ['key']
660
+ },
661
+ minItems: 1
662
+ },
663
+ dry_run: {
664
+ type: 'boolean',
665
+ description: 'If true, return a preview of changes without writing to disk',
666
+ default: false
667
+ }
668
+ },
669
+ required: ['library', 'updates']
670
+ }
671
+ },
672
+ {
673
+ name: 'list_templates',
674
+ description: 'List available prompt templates for quick creation.',
675
+ inputSchema: {
676
+ type: 'object',
677
+ properties: {
678
+ category: { type: 'string', description: 'Optional category filter (design, development, analysis, etc.)' },
679
+ search: { type: 'string', description: 'Optional text search over template key/name/description' }
680
+ },
681
+ required: []
682
+ }
683
+ },
684
+ {
685
+ name: 'get_template',
686
+ description: 'Get detailed information about a specific prompt template.',
687
+ inputSchema: {
688
+ type: 'object',
689
+ properties: {
690
+ key: { type: 'string', description: 'Template key' }
691
+ },
692
+ required: ['key']
693
+ }
694
+ },
695
+ {
696
+ name: 'create_from_template',
697
+ description: 'Create a new prompt from a template and save it into a local library via quick_create_prompt.',
698
+ inputSchema: {
699
+ type: 'object',
700
+ properties: {
701
+ template: { type: 'string', description: 'Template key' },
702
+ customize: {
703
+ type: 'object',
704
+ description: 'Values to substitute into the template variables'
705
+ },
706
+ library: {
707
+ type: 'string',
708
+ description: 'Target library name or CID (optional, default: My Library)'
709
+ },
710
+ name: {
711
+ type: 'string',
712
+ description: 'Override prompt display name (optional)'
713
+ }
714
+ },
715
+ required: ['template', 'customize']
716
+ }
717
+ },
718
+ {
719
+ name: 'analyze_dependencies',
720
+ description: 'Analyze variable usage across all prompts in a pinned library.',
721
+ inputSchema: {
722
+ type: 'object',
723
+ properties: {
724
+ library: {
725
+ type: 'string',
726
+ description: 'Pinned library name or CID'
727
+ },
728
+ analysis_type: {
729
+ type: 'string',
730
+ enum: ['variables', 'all'],
731
+ description: 'Type of analysis (v1 supports variables)',
732
+ default: 'variables'
733
+ }
734
+ },
735
+ required: ['library']
736
+ }
737
+ },
738
+ {
739
+ name: 'suggest_subdaos_for_library',
740
+ description: 'Suggest SubDAOs that might be a good fit for publishing a given local library, and provide CLI workflows for creating your own SubDAO and pushing the library.',
741
+ inputSchema: {
742
+ type: 'object',
743
+ properties: {
744
+ library: {
745
+ type: 'string',
746
+ description: 'Local pinned library name or CID'
747
+ },
748
+ limit: {
749
+ type: 'number',
750
+ description: 'Max SubDAOs to return (default 5)',
751
+ default: 5
752
+ },
753
+ mode_filter: {
754
+ type: 'string',
755
+ enum: ['any', 'creator', 'squad', 'community'],
756
+ description: 'Optional governance mode preference',
757
+ default: 'any'
758
+ }
759
+ },
760
+ required: ['library']
761
+ }
762
+ },
383
763
  {
384
764
  name: 'list_workspace_skills',
385
765
  description: 'List local workspace skills from prompts/ (e.g. prompts/skills/*.md)',
@@ -407,10 +787,10 @@ class SageMCPServer {
407
787
 
408
788
  // Structured logger to stderr (stdout must remain JSON-RPC only)
409
789
  this.log = pino ? pino({ level: process.env.MCP_DEBUG === 'true' ? 'debug' : 'info' }, pino.destination(2)) : {
410
- debug: (...a) => { if (process.env.MCP_DEBUG === 'true') try { console.error(...a); } catch(_){} },
411
- info: (...a) => { try { console.error(...a); } catch(_){} },
412
- warn: (...a) => { try { console.error(...a); } catch(_){} },
413
- error: (...a) => { try { console.error(...a); } catch(_){} },
790
+ debug: (...a) => { if (process.env.MCP_DEBUG === 'true') try { console.error(...a); } catch (_) { } },
791
+ info: (...a) => { try { console.error(...a); } catch (_) { } },
792
+ warn: (...a) => { try { console.error(...a); } catch (_) { } },
793
+ error: (...a) => { try { console.error(...a); } catch (_) { } },
414
794
  };
415
795
 
416
796
  // Shared provider + ABIs
@@ -500,6 +880,30 @@ class SageMCPServer {
500
880
  formatUnifiedResults: this.formatUnifiedResults,
501
881
  });
502
882
 
883
+ const { createQuickStart } = require('./services/mcp/quick-start');
884
+ this.quickStartHandler = createQuickStart({
885
+ libraryManager: this.libraryManager,
886
+ logger: this.log,
887
+ });
888
+
889
+ const { createBulkOperations } = require('./services/mcp/bulk-operations');
890
+ this.bulkOperations = createBulkOperations({
891
+ libraryManager: this.libraryManager,
892
+ logger: this.log,
893
+ });
894
+
895
+ const { createTemplateManager } = require('./services/mcp/template-manager');
896
+ this.templateManager = createTemplateManager({
897
+ quickStart: this.quickStartHandler,
898
+ logger: this.log,
899
+ });
900
+
901
+ const { createDependencyAnalyzer } = require('./services/mcp/dependency-analyzer');
902
+ this.dependencyAnalyzer = createDependencyAnalyzer({
903
+ libraryManager: this.libraryManager,
904
+ logger: this.log,
905
+ });
906
+
503
907
  this.libraryBindingsManager = createLibraryBindingsManager({
504
908
  provider: this.provider,
505
909
  subdaoAbi: this.subdaoAbi,
@@ -573,6 +977,15 @@ class SageMCPServer {
573
977
  'tool:search_prompts': (params) => this.searchPromptsUnified(params),
574
978
  'tool:search_onchain_prompts': (params) => this.searchOnchainPrompts(params),
575
979
  'tool:trending_prompts': (params) => this.trendingPrompts(params),
980
+ 'tool:list_prompts': (params) => this.listPrompts(params),
981
+ 'tool:get_prompt': (params) => this.getPrompt(params),
982
+ 'tool:test_prompt': (params) => this.testPrompt(params),
983
+ 'tool:quick_create_prompt': (params) => this.quickCreatePrompt(params),
984
+ 'tool:quick_iterate_prompt': (params) => this.quickIteratePrompt(params),
985
+ 'tool:improve_prompt': (params) => this.improvePrompt(params),
986
+ 'tool:rename_prompt': (params) => this.renamePrompt(params),
987
+ 'tool:quick_test_prompt': (params) => this.testPrompt(params),
988
+ 'tool:help': (params) => this.getHelp(params),
576
989
  'tool:list_libraries': (params) => this.listLibraries(params),
577
990
  'tool:get_prompt_content': (params) => this.getPromptContent(params),
578
991
  'tool:list_subdaos': () => this.listSubDAOs(),
@@ -595,6 +1008,13 @@ class SageMCPServer {
595
1008
  'tool:refresh_library_bindings': (params) => this.refreshLibraryBindings(params),
596
1009
  'tool:list_workspace_skills': (params) => this.listWorkspaceSkills(params),
597
1010
  'tool:get_workspace_skill': (params) => this.getWorkspaceSkill(params),
1011
+ 'tool:update_library_metadata': (params) => this.updateLibraryMetadata(params),
1012
+ 'tool:bulk_update_prompts': (params) => this.bulkUpdatePrompts(params),
1013
+ 'tool:list_templates': (params) => this.listTemplates(params),
1014
+ 'tool:get_template': (params) => this.getTemplate(params),
1015
+ 'tool:create_from_template': (params) => this.createPromptFromTemplate(params),
1016
+ 'tool:analyze_dependencies': (params) => this.analyzeDependencies(params),
1017
+ 'tool:suggest_subdaos_for_library': (params) => this.suggestSubdaosForLibrary(params),
598
1018
  });
599
1019
 
600
1020
  const toolHandlers = {
@@ -646,7 +1066,7 @@ class SageMCPServer {
646
1066
  if (process.env.SAGE_DEBUG_DISCOVERY === '1') {
647
1067
  try {
648
1068
  console.warn('mcp_discovery_client_failed', error.message);
649
- } catch (_) {}
1069
+ } catch (_) { }
650
1070
  }
651
1071
  }
652
1072
  return this.discoveryClient;
@@ -683,7 +1103,7 @@ class SageMCPServer {
683
1103
  if (process.env.SAGE_DEBUG_DISCOVERY === '1') {
684
1104
  try {
685
1105
  console.warn('mcp_discovery_event_failed', error.message);
686
- } catch (_) {}
1106
+ } catch (_) { }
687
1107
  }
688
1108
  }
689
1109
  }
@@ -727,102 +1147,34 @@ class SageMCPServer {
727
1147
  * - Skill files live under promptsDir/skills/*.md
728
1148
  */
729
1149
  listWorkspaceSkills() {
730
- const skills = [];
731
1150
  try {
732
1151
  const { readWorkspace, DEFAULT_DIR } = require('./services/prompts/workspace');
1152
+ const { findWorkspaceSkills } = require('./services/skills/discovery');
733
1153
  const ws = readWorkspace() || {};
734
1154
  const promptsDir = ws.promptsDir || DEFAULT_DIR || 'prompts';
735
- const baseDir = path.join(process.cwd(), promptsDir, 'skills');
736
- if (!fs.existsSync(baseDir)) {
737
- return {
738
- content: [
739
- {
740
- type: 'text',
741
- text: 'No workspace skills directory found (expected prompts/skills). Create prompts/skills/<name>.md to define skills for this repo.',
742
- },
743
- ],
744
- };
745
- }
746
- const walk = (dir) => {
747
- const entries = fs.readdirSync(dir, { withFileTypes: true });
748
- for (const entry of entries) {
749
- const full = path.join(dir, entry.name);
750
- if (entry.isDirectory()) {
751
- walk(full);
752
- } else if (entry.isFile() && full.toLowerCase().endsWith('.md')) {
753
- skills.push(full);
754
- }
755
- }
756
- };
757
- walk(baseDir);
758
- const results = skills.map((filePath) => {
759
- const relFromPrompts = path.relative(path.join(process.cwd(), promptsDir), filePath);
760
- const key = relFromPrompts.replace(/\\/g, '/').replace(/\.md$/i, '');
761
- let title = path.basename(filePath, '.md');
762
- let summary = '';
763
- let tags = [];
764
- try {
765
- const raw = fs.readFileSync(filePath, 'utf8');
766
- if (raw.startsWith('---')) {
767
- const end = raw.indexOf('\n---', 3);
768
- if (end !== -1) {
769
- const front = raw.slice(3, end).split(/\r?\n/);
770
- for (const line of front) {
771
- const idx = line.indexOf(':');
772
- if (idx === -1) continue;
773
- const k = line.slice(0, idx).trim().toLowerCase();
774
- let v = line.slice(idx + 1).trim();
775
- if (k === 'title' && v) title = v;
776
- if (k === 'summary' && v) summary = v;
777
- if (k === 'tags' && v) {
778
- try {
779
- tags = JSON.parse(v.replace(/'/g, '"'));
780
- } catch (_) {
781
- tags = String(v)
782
- .split(/[,|\s]+/)
783
- .filter(Boolean);
784
- }
785
- }
786
- }
787
- }
788
- }
789
- } catch (_) {
790
- // ignore parse errors; fall back to filename
791
- }
792
- return {
793
- key,
794
- name: title,
795
- summary,
796
- tags,
797
- path: filePath,
798
- };
799
- });
800
-
1155
+ const results = findWorkspaceSkills({ promptsDir });
801
1156
  if (!results.length) {
802
1157
  return {
803
1158
  content: [
804
1159
  {
805
1160
  type: 'text',
806
- text: 'No skills found under prompts/skills. Create prompts/skills/<name>.md to define repo-specific skills.',
1161
+ text: 'No skills found. Create prompts/skills/<name>.md or prompts/skills/<name>/SKILL.md to define skills for this repo.',
807
1162
  },
808
- { type: 'json', text: JSON.stringify({ skills: [] }, null, 2) },
1163
+ { type: 'text', text: '```json\n' + JSON.stringify({ skills: [] }, null, 2) + '\n```' },
809
1164
  ],
810
1165
  };
811
1166
  }
812
-
813
1167
  const textLines = results
814
1168
  .map(
815
1169
  (s, idx) =>
816
- `${idx + 1}. **${s.name}** (${s.key})\n 📁 ${path.relative(process.cwd(), s.path)}${
817
- s.summary ? `\n 📝 ${s.summary}` : ''
1170
+ `${idx + 1}. **${s.name}** (${s.key})\n 📁 ${path.relative(process.cwd(), s.path)}${s.summary ? `\n 📝 ${s.summary}` : ''
818
1171
  }${s.tags && s.tags.length ? `\n 🔖 ${s.tags.join(', ')}` : ''}`,
819
1172
  )
820
1173
  .join('\n\n');
821
-
822
1174
  return {
823
1175
  content: [
824
1176
  { type: 'text', text: `Workspace skills (${results.length})\n\n${textLines}` },
825
- { type: 'json', text: JSON.stringify({ skills: results }, null, 2) },
1177
+ { type: 'text', text: '```json\n' + JSON.stringify({ skills: results }, null, 2) + '\n```' },
826
1178
  ],
827
1179
  };
828
1180
  } catch (error) {
@@ -839,42 +1191,38 @@ class SageMCPServer {
839
1191
  getWorkspaceSkill({ key }) {
840
1192
  try {
841
1193
  if (!key || !String(key).trim()) {
842
- return {
843
- content: [{ type: 'text', text: 'get_workspace_skill: key is required' }],
844
- };
1194
+ return { content: [{ type: 'text', text: 'get_workspace_skill: key is required' }] };
845
1195
  }
846
1196
  const { readWorkspace, DEFAULT_DIR } = require('./services/prompts/workspace');
1197
+ const { resolveSkillFileByKey } = require('./services/skills/discovery');
847
1198
  const ws = readWorkspace() || {};
848
1199
  const promptsDir = ws.promptsDir || DEFAULT_DIR || 'prompts';
849
1200
  const safeKey = String(key).trim().replace(/^\/+/, '').replace(/\.md$/i, '');
850
- const filePath = path.join(process.cwd(), promptsDir, `${safeKey}.md`);
851
- if (!fs.existsSync(filePath)) {
1201
+ const resolved = resolveSkillFileByKey({ promptsDir, key: safeKey });
1202
+ if (!resolved || !fs.existsSync(resolved.path)) {
1203
+ const expectedFlat = path.join(process.cwd(), promptsDir, `${safeKey}.md`);
1204
+ const expectedDir = path.join(process.cwd(), promptsDir, safeKey, 'SKILL.md');
852
1205
  return {
853
1206
  content: [
854
1207
  {
855
1208
  type: 'text',
856
- text: `Workspace skill not found for key '${safeKey}'. Expected file at ${path.relative(
857
- process.cwd(),
858
- filePath,
859
- )}`,
1209
+ text: `Workspace skill not found for key '${safeKey}'. Expected at ${path.relative(process.cwd(), expectedFlat)} or ${path.relative(process.cwd(), expectedDir)}`,
860
1210
  },
861
1211
  ],
862
1212
  };
863
1213
  }
864
- const body = fs.readFileSync(filePath, 'utf8');
1214
+ const body = fs.readFileSync(resolved.path, 'utf8');
865
1215
  return {
866
1216
  content: [
867
1217
  {
868
1218
  type: 'text',
869
- text: `Loaded workspace skill '${safeKey}' from ${path.relative(process.cwd(), filePath)}.\n\n${body}`,
1219
+ text: `Loaded workspace skill '${safeKey}' from ${path.relative(process.cwd(), resolved.path)}.\n\n${body}`,
870
1220
  },
871
- { type: 'json', text: JSON.stringify({ key: safeKey, path: filePath, body }, null, 2) },
1221
+ { type: 'text', text: '```json\n' + JSON.stringify({ key: safeKey, path: resolved.path, baseDir: resolved.baseDir, body }, null, 2) + '\n```' },
872
1222
  ],
873
1223
  };
874
1224
  } catch (error) {
875
- return {
876
- content: [{ type: 'text', text: `Error loading workspace skill: ${error.message}` }],
877
- };
1225
+ return { content: [{ type: 'text', text: `Error loading workspace skill: ${error.message}` }] };
878
1226
  }
879
1227
  }
880
1228
 
@@ -951,7 +1299,7 @@ class SageMCPServer {
951
1299
  libraryRegistryAddress: addressResolution.resolveRegistryAddress().registry,
952
1300
  searchSubDAOPrompts: this.searchSubDAOPrompts.bind(this),
953
1301
  fetchManifestPrompts: this.fetchManifestPrompts.bind(this),
954
- });
1302
+ });
955
1303
  return buildOnchainSearchResponse(prompts);
956
1304
  } catch (error) {
957
1305
  return {
@@ -969,6 +1317,839 @@ class SageMCPServer {
969
1317
  return this.searchPromptsUnifiedHandler(options);
970
1318
  }
971
1319
 
1320
+ async listPrompts({ source = 'local', library = '', limit = 20 } = {}) {
1321
+ try {
1322
+ const results = await this.searchPromptsUnifiedHandler({
1323
+ query: '',
1324
+ source,
1325
+ includeContent: false,
1326
+ page: 1,
1327
+ pageSize: limit
1328
+ });
1329
+
1330
+ if (library && results?.content?.[1]?.text) {
1331
+ const jsonMatch = results.content[1].text.match(/```json\n([\s\S]+)\n```/);
1332
+ if (jsonMatch) {
1333
+ const data = JSON.parse(jsonMatch[1]);
1334
+ const libraryLower = library.toLowerCase();
1335
+ data.results = data.results.filter(r =>
1336
+ r.library?.name?.toLowerCase().includes(libraryLower) ||
1337
+ r.library?.cid?.toLowerCase().includes(libraryLower)
1338
+ );
1339
+ data.total = data.results.length;
1340
+
1341
+ const formatted = this.formatUnifiedResults(data.results, { total: data.total, page: 1, pageSize: limit });
1342
+ return {
1343
+ content: [
1344
+ { type: 'text', text: formatted },
1345
+ { type: 'text', text: '```json\n' + JSON.stringify(data, null, 2) + '\n```' }
1346
+ ]
1347
+ };
1348
+ }
1349
+ }
1350
+
1351
+ return results;
1352
+ } catch (error) {
1353
+ return { content: [{ type: 'text', text: `Error listing prompts: ${error.message}` }] };
1354
+ }
1355
+ }
1356
+
1357
+ async getPrompt({ key, library = '' } = {}) {
1358
+ try {
1359
+ if (!key) {
1360
+ return { content: [{ type: 'text', text: 'Error: key parameter is required' }] };
1361
+ }
1362
+
1363
+ const results = await this.searchPromptsUnifiedHandler({
1364
+ query: key,
1365
+ source: 'local',
1366
+ includeContent: true,
1367
+ pageSize: 50
1368
+ });
1369
+
1370
+ if (results?.content?.[1]?.text) {
1371
+ const jsonMatch = results.content[1].text.match(/```json\n([\s\S]+)\n```/);
1372
+ if (jsonMatch) {
1373
+ const data = JSON.parse(jsonMatch[1]);
1374
+ let prompt = data.results.find(r => r.key === key);
1375
+
1376
+ if (library && !prompt) {
1377
+ const libraryLower = library.toLowerCase();
1378
+ prompt = data.results.find(r =>
1379
+ r.key === key && (r.library?.name?.toLowerCase().includes(libraryLower) || r.library?.cid?.toLowerCase().includes(libraryLower))
1380
+ );
1381
+ }
1382
+
1383
+ if (!prompt) {
1384
+ prompt = data.results.find(r => r.key?.includes(key));
1385
+ }
1386
+
1387
+ if (!prompt) {
1388
+ return { content: [{ type: 'text', text: `No prompt found with key "${key}"` }] };
1389
+ }
1390
+
1391
+ const text = `**${prompt.name}**\n\n🔑 Key: ${prompt.key}\n📚 Library: ${prompt.library?.name || 'Unknown'}\n📄 ${prompt.description || 'No description'}\n🏷️ Tags: ${prompt.tags?.join(', ') || 'None'}\n\n**Content:**\n\`\`\`\n${prompt.content || '(No content)'}\n\`\`\``;
1392
+
1393
+ return {
1394
+ content: [
1395
+ { type: 'text', text },
1396
+ { type: 'text', text: '```json\n' + JSON.stringify(prompt, null, 2) + '\n```' }
1397
+ ]
1398
+ };
1399
+ }
1400
+ }
1401
+
1402
+ return { content: [{ type: 'text', text: `No prompt found with key "${key}"` }] };
1403
+ } catch (error) {
1404
+ return { content: [{ type: 'text', text: `Error getting prompt: ${error.message}` }] };
1405
+ }
1406
+ }
1407
+
1408
+ async testPrompt({ key, library = '', variables = {} } = {}) {
1409
+ try {
1410
+ if (!key) {
1411
+ return { content: [{ type: 'text', text: 'Error: key parameter is required' }] };
1412
+ }
1413
+
1414
+ const promptResult = await this.getPrompt({ key, library });
1415
+ if (!promptResult?.content?.[1]?.text) {
1416
+ return promptResult;
1417
+ }
1418
+
1419
+ const jsonMatch = promptResult.content[1].text.match(/```json\n([\s\S]+)\n```/);
1420
+ if (!jsonMatch) {
1421
+ return { content: [{ type: 'text', text: 'Error: Failed to parse prompt data' }] };
1422
+ }
1423
+
1424
+ const prompt = JSON.parse(jsonMatch[1]);
1425
+ let content = prompt.content || '';
1426
+
1427
+ if (!content) {
1428
+ return { content: [{ type: 'text', text: `Prompt "${key}" has no content to test` }] };
1429
+ }
1430
+
1431
+ const variablePattern = /\$\{([^}]+)\}/g;
1432
+ const foundVariables = [];
1433
+ let match;
1434
+ while ((match = variablePattern.exec(content)) !== null) {
1435
+ if (!foundVariables.includes(match[1])) {
1436
+ foundVariables.push(match[1]);
1437
+ }
1438
+ }
1439
+
1440
+ let renderedContent = content;
1441
+ const substitutions = {};
1442
+
1443
+ for (const varName of foundVariables) {
1444
+ if (variables[varName] !== undefined) {
1445
+ renderedContent = renderedContent.replace(new RegExp(`\\$\\{${varName}\\}`, 'g'), variables[varName]);
1446
+ substitutions[varName] = variables[varName];
1447
+ }
1448
+ }
1449
+
1450
+ const missingVars = foundVariables.filter(v => variables[v] === undefined);
1451
+
1452
+ let result = `**Testing: ${prompt.name}**\n\n`;
1453
+ if (foundVariables.length === 0) {
1454
+ result += '✅ No variables\n\n';
1455
+ } else {
1456
+ result += `**Variables:** ${foundVariables.join(', ')}\n`;
1457
+ if (Object.keys(substitutions).length > 0) {
1458
+ result += `**Substituted:** ${Object.keys(substitutions).join(', ')}\n`;
1459
+ }
1460
+ if (missingVars.length > 0) {
1461
+ result += `**⚠️ Missing:** ${missingVars.join(', ')}\n`;
1462
+ }
1463
+ result += '\n';
1464
+ }
1465
+
1466
+ result += `**Rendered:**\n\`\`\`\n${renderedContent}\n\`\`\``;
1467
+
1468
+ return {
1469
+ content: [
1470
+ { type: 'text', text: result },
1471
+ { type: 'text', text: '```json\n' + JSON.stringify({ prompt: { key: prompt.key, name: prompt.name }, variables: { found: foundVariables, substituted: Object.keys(substitutions), missing: missingVars }, rendered: renderedContent }, null, 2) + '\n```' }
1472
+ ]
1473
+ };
1474
+ } catch (error) {
1475
+ return { content: [{ type: 'text', text: `Error testing prompt: ${error.message}` }] };
1476
+ }
1477
+ }
1478
+
1479
+ async updateLibraryMetadata(params) {
1480
+ try {
1481
+ const result = await this.bulkOperations.updateLibraryMetadata(params);
1482
+ const textLines = [
1483
+ '✅ Updated library metadata',
1484
+ `Library: ${result.library}`,
1485
+ `CID: ${result.cid}`,
1486
+ `Fields: ${result.updatedLibraryFields.length ? result.updatedLibraryFields.join(', ') : '(none)'}`,
1487
+ `Prompts touched: ${result.updatedPromptCount}`,
1488
+ ];
1489
+ return {
1490
+ content: [
1491
+ { type: 'text', text: textLines.join('\n') },
1492
+ { type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' },
1493
+ ],
1494
+ };
1495
+ } catch (error) {
1496
+ return { content: [{ type: 'text', text: `Error updating library metadata: ${error.message}` }] };
1497
+ }
1498
+ }
1499
+
1500
+ async bulkUpdatePrompts(params) {
1501
+ try {
1502
+ const result = await this.bulkOperations.bulkUpdatePrompts(params);
1503
+ const header = result.dryRun ? '✅ Dry run: no changes written' : '✅ Bulk prompt update complete';
1504
+ const textLines = [
1505
+ header,
1506
+ `Library: ${result.library}`,
1507
+ `CID: ${result.cid}`,
1508
+ `Updated prompts: ${result.updatedCount}`,
1509
+ ];
1510
+ if (result.missingKeys?.length) {
1511
+ textLines.push(`Missing keys: ${result.missingKeys.join(', ')}`);
1512
+ }
1513
+ return {
1514
+ content: [
1515
+ { type: 'text', text: textLines.join('\n') },
1516
+ { type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' },
1517
+ ],
1518
+ };
1519
+ } catch (error) {
1520
+ return { content: [{ type: 'text', text: `Error bulk updating prompts: ${error.message}` }] };
1521
+ }
1522
+ }
1523
+
1524
+ async listTemplates(params = {}) {
1525
+ try {
1526
+ const result = this.templateManager.listTemplates(params || {});
1527
+ const items = result.templates || [];
1528
+ if (!items.length) {
1529
+ return { content: [{ type: 'text', text: 'No templates available.' }] };
1530
+ }
1531
+ const lines = [];
1532
+ lines.push(`Available templates (${items.length}):`);
1533
+ lines.push('');
1534
+ for (const tpl of items) {
1535
+ lines.push(`- **${tpl.name}** (${tpl.key}) [${tpl.category || 'uncategorized'}]`);
1536
+ if (tpl.summary) {
1537
+ lines.push(` ${tpl.summary}`);
1538
+ }
1539
+ if (tpl.requiredVariables?.length) {
1540
+ lines.push(` Required: ${tpl.requiredVariables.join(', ')}`);
1541
+ }
1542
+ lines.push('');
1543
+ }
1544
+ return {
1545
+ content: [
1546
+ { type: 'text', text: lines.join('\n') },
1547
+ { type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' },
1548
+ ],
1549
+ };
1550
+ } catch (error) {
1551
+ return { content: [{ type: 'text', text: `Error listing templates: ${error.message}` }] };
1552
+ }
1553
+ }
1554
+
1555
+ async getTemplate(params) {
1556
+ try {
1557
+ const result = this.templateManager.getTemplate(params || {});
1558
+ return {
1559
+ content: [
1560
+ {
1561
+ type: 'text',
1562
+ text:
1563
+ `Template: ${result.template.name} (${result.template.key})\n` +
1564
+ `Category: ${result.template.category || 'uncategorized'}\n\n` +
1565
+ `${result.template.description || ''}`,
1566
+ },
1567
+ { type: 'text', text: '```json\n' + JSON.stringify(result.template, null, 2) + '\n```' },
1568
+ ],
1569
+ };
1570
+ } catch (error) {
1571
+ return { content: [{ type: 'text', text: `Error getting template: ${error.message}` }] };
1572
+ }
1573
+ }
1574
+
1575
+ async createPromptFromTemplate(params) {
1576
+ try {
1577
+ const result = await this.templateManager.createFromTemplate(params || {});
1578
+ const quick = result.quickCreate || {};
1579
+ const lines = [];
1580
+ lines.push('✅ Created prompt from template');
1581
+ lines.push(`Template: ${result.template}`);
1582
+ lines.push(`Key: ${quick.key || '(unknown key)'}`);
1583
+ lines.push(`Library: ${quick.library || '(unknown library)'}`);
1584
+ return {
1585
+ content: [
1586
+ { type: 'text', text: lines.join('\n') },
1587
+ { type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' },
1588
+ ],
1589
+ };
1590
+ } catch (error) {
1591
+ return { content: [{ type: 'text', text: `Error creating prompt from template: ${error.message}` }] };
1592
+ }
1593
+ }
1594
+
1595
+ async analyzeDependencies(params) {
1596
+ try {
1597
+ return await this.dependencyAnalyzer.analyzeDependencies(params || {});
1598
+ } catch (error) {
1599
+ return { content: [{ type: 'text', text: `Error analyzing dependencies: ${error.message}` }] };
1600
+ }
1601
+ }
1602
+
1603
+ async suggestSubdaosForLibrary({ library, limit = 5, mode_filter = 'any' } = {}) {
1604
+ try {
1605
+ const lm = this.libraryManager;
1606
+ const pinned = lm.listPinned() || [];
1607
+ const id = String(library || '').trim();
1608
+ if (!id) {
1609
+ return { content: [{ type: 'text', text: 'Error: library parameter is required' }] };
1610
+ }
1611
+
1612
+ let selected = null;
1613
+ let row = pinned.find((r) => r.cid === id);
1614
+ if (row) {
1615
+ selected = lm.loadPinned(row.cid);
1616
+ selected.row = row;
1617
+ } else {
1618
+ const lower = id.toLowerCase();
1619
+ const matches = [];
1620
+ for (const r of pinned) {
1621
+ try {
1622
+ const loaded = lm.loadPinned(r.cid);
1623
+ const libName = String(loaded.manifest?.library?.name || r.name || '').toLowerCase();
1624
+ if (libName === lower) {
1625
+ matches.push({ row: r, ...loaded });
1626
+ }
1627
+ } catch (_) {
1628
+ // ignore corrupt
1629
+ }
1630
+ }
1631
+ if (matches.length === 0) {
1632
+ return {
1633
+ content: [
1634
+ {
1635
+ type: 'text',
1636
+ text: `No pinned library found matching "${library}". Use list_libraries(source="local") to see available libraries.`,
1637
+ },
1638
+ ],
1639
+ };
1640
+ }
1641
+ if (matches.length > 1) {
1642
+ const lines = matches.map(
1643
+ (m) => `- ${m.row.cid} (${m.manifest?.library?.name || m.row.name || 'unnamed'})`,
1644
+ );
1645
+ return {
1646
+ content: [
1647
+ {
1648
+ type: 'text',
1649
+ text:
1650
+ `Ambiguous library match for "${library}". Candidates:\n` +
1651
+ `${lines.join('\n')}\n\nPass an explicit CID to disambiguate.`,
1652
+ },
1653
+ ],
1654
+ };
1655
+ }
1656
+ selected = matches[0];
1657
+ }
1658
+
1659
+ const manifest = selected.manifest;
1660
+ const libName = manifest.library?.name || selected.row?.name || library;
1661
+ const libDescription = manifest.library?.description || '';
1662
+ const tagsSet = new Set(
1663
+ (manifest.library?.tags || []).map((t) => String(t).toLowerCase().trim()).filter(Boolean),
1664
+ );
1665
+ for (const p of manifest.prompts || []) {
1666
+ if (!p || !Array.isArray(p.tags)) continue;
1667
+ for (const t of p.tags) {
1668
+ const v = String(t).toLowerCase().trim();
1669
+ if (v) tagsSet.add(v);
1670
+ }
1671
+ }
1672
+ const libTags = Array.from(tagsSet);
1673
+
1674
+ const subdaos = await this.getSubDAOList({ limit: 50 });
1675
+ if (!subdaos || !subdaos.length) {
1676
+ const fallbackText =
1677
+ 'No SubDAOs discovered from subgraph/factory.\n\n' +
1678
+ 'You can still create your own personal SubDAO for this library:\n\n' +
1679
+ `1) Create personal SubDAO:\n sage subdao create --type personal --name "${libName}" --description "${libDescription || libName}" --bootstrap\n\n` +
1680
+ '2) Scaffold and push a manifest:\n' +
1681
+ ' sage library scaffold-manifest courtyard-lib/manifest.json \\\n' +
1682
+ ` --name "${libName}" \\\n` +
1683
+ ` --description "${libDescription || libName}"\n` +
1684
+ ' # Add prompts with: sage library add-prompt --manifest courtyard-lib/manifest.json --file prompts/<file>.md --key <key>\n' +
1685
+ ' sage library push courtyard-lib/manifest.json --subdao 0xYourSubDAO --pin --wait\n';
1686
+ return { content: [{ type: 'text', text: fallbackText }] };
1687
+ }
1688
+
1689
+ const modeFilter = mode_filter || 'any';
1690
+ const scored = [];
1691
+ for (const s of subdaos) {
1692
+ const name = String(s.name || '').toLowerCase();
1693
+ const desc = String(s.description || '').toLowerCase();
1694
+ const tokens = new Set(
1695
+ (name + ' ' + desc)
1696
+ .split(/[^a-z0-9]+/i)
1697
+ .map((t) => t.toLowerCase().trim())
1698
+ .filter(Boolean),
1699
+ );
1700
+ let matchCount = 0;
1701
+ const matchedTags = [];
1702
+ for (const tag of libTags) {
1703
+ if (tokens.has(tag)) {
1704
+ matchCount += 1;
1705
+ matchedTags.push(tag);
1706
+ }
1707
+ }
1708
+
1709
+ if (modeFilter !== 'any') {
1710
+ // Mode-specific filtering can be added in a future iteration.
1711
+ }
1712
+
1713
+ const libraryCount = Array.isArray(s.registries) ? s.registries.length : 0;
1714
+ const fitScore = matchCount + libraryCount * 0.1;
1715
+ scored.push({
1716
+ address: s.address,
1717
+ name: s.name || `SubDAO-${String(s.address).slice(-6)}`,
1718
+ description: s.description || '',
1719
+ registryAddress: s.registryAddress || null,
1720
+ defaultLibraryId: s.defaultLibraryId || 'main',
1721
+ libraryCount,
1722
+ matchedTags,
1723
+ fitScore,
1724
+ });
1725
+ }
1726
+
1727
+ scored.sort((a, b) => (b.fitScore || 0) - (a.fitScore || 0));
1728
+ const top = scored.slice(0, limit);
1729
+
1730
+ const lines = [];
1731
+ lines.push(`🎯 Suggested SubDAOs for library "${libName}"`);
1732
+ lines.push('');
1733
+
1734
+ if (!top.length || !libTags.length) {
1735
+ lines.push('No strong SubDAO matches found based on tags and names.');
1736
+ } else {
1737
+ top.forEach((s, idx) => {
1738
+ lines.push(`${idx + 1}. ${s.name} (${s.address})`);
1739
+ if (s.matchedTags.length) {
1740
+ lines.push(` • Tag overlap: ${s.matchedTags.join(', ')}`);
1741
+ }
1742
+ lines.push(` • Libraries: ${s.libraryCount}`);
1743
+ if (s.description) {
1744
+ lines.push(` • Description: ${s.description}`);
1745
+ }
1746
+ lines.push('');
1747
+ });
1748
+ }
1749
+
1750
+ lines.push('---');
1751
+ lines.push('💡 If none of these feel right, you can create your own personal SubDAO and publish there:');
1752
+ lines.push('');
1753
+ lines.push('1) Create a personal SubDAO for this library:');
1754
+ lines.push(
1755
+ ` sage subdao create --type personal --name "${libName}" --description "${libDescription || libName}" --bootstrap`,
1756
+ );
1757
+ lines.push('');
1758
+ lines.push('2) Scaffold a manifest and push it:');
1759
+ lines.push(' sage library scaffold-manifest courtyard-lib/manifest.json \\');
1760
+ lines.push(` --name "${libName}" \\`);
1761
+ lines.push(` --description "${libDescription || libName}"`);
1762
+ lines.push(
1763
+ ' # Add prompts with: sage library add-prompt --manifest courtyard-lib/manifest.json --file prompts/<file>.md --key <key>',
1764
+ );
1765
+ lines.push(
1766
+ ' sage library push courtyard-lib/manifest.json --subdao 0xYourSubDAO --pin --wait',
1767
+ );
1768
+
1769
+ const cliWorkflows = {
1770
+ createPersonalSubdao: {
1771
+ description: 'Create a personal SubDAO for this library.',
1772
+ commands: [
1773
+ `sage subdao create --type personal --name "${libName}" --description "${libDescription || libName}" --bootstrap`,
1774
+ ],
1775
+ },
1776
+ createLibraryAndPush: {
1777
+ description: 'Scaffold a manifest from your local prompts and push it to a SubDAO.',
1778
+ commands: [
1779
+ '# Scaffold manifest (adjust path/name as needed):',
1780
+ 'sage library scaffold-manifest courtyard-lib/manifest.json \\',
1781
+ ` --name "${libName}" \\`,
1782
+ ` --description "${libDescription || libName}"`,
1783
+ '',
1784
+ '# Add prompts (repeat per prompt):',
1785
+ 'sage library add-prompt --manifest courtyard-lib/manifest.json \\',
1786
+ ' --file prompts/<file>.md \\',
1787
+ ' --key <key> \\',
1788
+ ' --name "<Prompt Name>"',
1789
+ '',
1790
+ '# Push to your SubDAO (replace 0xYourSubDAO):',
1791
+ 'sage library push courtyard-lib/manifest.json --subdao 0xYourSubDAO --pin --wait',
1792
+ ],
1793
+ },
1794
+ };
1795
+
1796
+ const jsonPayload = {
1797
+ library: {
1798
+ cid: selected.row?.cid,
1799
+ name: libName,
1800
+ tags: libTags,
1801
+ },
1802
+ suggestions: top,
1803
+ cliWorkflows,
1804
+ };
1805
+
1806
+ return {
1807
+ content: [
1808
+ { type: 'text', text: lines.join('\n') },
1809
+ { type: 'text', text: '```json\n' + JSON.stringify(jsonPayload, null, 2) + '\n```' },
1810
+ ],
1811
+ };
1812
+ } catch (error) {
1813
+ return { content: [{ type: 'text', text: `Error suggesting SubDAOs: ${error.message}` }] };
1814
+ }
1815
+ }
1816
+
1817
+ async improvePrompt({ key, library = '', pass = 'single', focus = 'all' } = {}) {
1818
+ try {
1819
+ if (!key) {
1820
+ return { content: [{ type: 'text', text: 'Error: key parameter is required' }] };
1821
+ }
1822
+
1823
+ const promptResult = await this.getPrompt({ key, library });
1824
+ if (!promptResult?.content?.[1]?.text) {
1825
+ return promptResult;
1826
+ }
1827
+
1828
+ const jsonMatch = promptResult.content[1].text.match(/```json\n([\s\S]+)\n```/);
1829
+ if (!jsonMatch) {
1830
+ return { content: [{ type: 'text', text: 'Error: Failed to parse prompt data for improvement' }] };
1831
+ }
1832
+
1833
+ const prompt = JSON.parse(jsonMatch[1]);
1834
+ const content = String(prompt.content || '');
1835
+ const description = String(prompt.description || '');
1836
+
1837
+ if (!content.trim()) {
1838
+ return { content: [{ type: 'text', text: `Prompt "${key}" has no content to analyze` }] };
1839
+ }
1840
+
1841
+ // Variable analysis
1842
+ const varPattern = /\$\{([^}]+)\}/g;
1843
+ const foundVars = [];
1844
+ let m;
1845
+ while ((m = varPattern.exec(content)) !== null) {
1846
+ const v = m[1];
1847
+ if (!foundVars.includes(v)) foundVars.push(v);
1848
+ }
1849
+
1850
+ // Simple heuristics
1851
+ const lengthChars = content.length;
1852
+ const lines = content.split(/\r?\n/).length;
1853
+ const mentionsOutput =
1854
+ /output|format|respond|response|return\b/i.test(content);
1855
+ const mentionsRole =
1856
+ /you are\b|as an? /i.test(content);
1857
+
1858
+ const improvementAreas = [];
1859
+ if (!mentionsOutput) {
1860
+ improvementAreas.push('Specify the expected output format (structure, tone, and level of detail).');
1861
+ }
1862
+ if (!mentionsRole) {
1863
+ improvementAreas.push('Clarify the assistant role (e.g. "You are a landscape design assistant...").');
1864
+ }
1865
+ if (!foundVars.length) {
1866
+ improvementAreas.push('Consider parameterizing important details with ${variables} to make the prompt reusable.');
1867
+ }
1868
+ if (foundVars.length && !description.toLowerCase().includes('variable')) {
1869
+ improvementAreas.push('Document variables in the description so users know what to provide.');
1870
+ }
1871
+ if (lengthChars < 300) {
1872
+ improvementAreas.push('Prompt is quite short; consider adding more context and edge-case handling.');
1873
+ }
1874
+
1875
+ const focusNote = focus && focus !== 'all'
1876
+ ? `\nFocus requested: **${focus}**\n`
1877
+ : '';
1878
+
1879
+ const baseInterviewQuestions = [
1880
+ 'What edge cases or failure modes do you most want this prompt to handle?',
1881
+ 'Should the prompt enforce a specific output format (e.g. bullet list, JSON, sections)?',
1882
+ 'Are there any important variables (budget, constraints, region, style, etc.) that are currently missing?',
1883
+ 'Should the prompt ask clarifying questions when information is missing, or make reasonable assumptions?',
1884
+ 'Is this prompt meant for a single use-case, or should it be adaptable across multiple related tasks?',
1885
+ ];
1886
+
1887
+ const variableQuestions = [
1888
+ 'Are the variable names clear and self-explanatory to future users?',
1889
+ 'Should any currently hard-coded values be turned into variables?',
1890
+ ];
1891
+
1892
+ const edgeCaseQuestions = [
1893
+ 'What should happen if key inputs are missing, contradictory, or clearly invalid?',
1894
+ 'Are there any \"must not happen\" outcomes you want to guard against?',
1895
+ ];
1896
+
1897
+ const outputQualityQuestions = [
1898
+ 'Do you want the model to always respond with a consistent structure (sections, headings, bullet points)?',
1899
+ 'Would example outputs help clarify what a great response looks like?',
1900
+ ];
1901
+
1902
+ const reusabilityQuestions = [
1903
+ 'Should this prompt be reusable across multiple projects or tightly scoped to one?',
1904
+ 'Are there any project-specific details that should be parameterized instead of hard-coded?',
1905
+ ];
1906
+
1907
+ // Select interview questions based on focus
1908
+ let interviewQuestions = baseInterviewQuestions.slice();
1909
+ if (focus === 'variables' || focus === 'all') interviewQuestions = interviewQuestions.concat(variableQuestions);
1910
+ if (focus === 'edge-cases' || focus === 'all') interviewQuestions = interviewQuestions.concat(edgeCaseQuestions);
1911
+ if (focus === 'output-quality' || focus === 'all') interviewQuestions = interviewQuestions.concat(outputQualityQuestions);
1912
+ if (focus === 'reusability' || focus === 'all') interviewQuestions = interviewQuestions.concat(reusabilityQuestions);
1913
+
1914
+ let text = `**Prompt Improvement Analysis**\n\n`;
1915
+ text += `**Name:** ${prompt.name || key}\n`;
1916
+ text += `**Key (stable ID):** ${prompt.key || key}\n`;
1917
+ text += `**Library:** ${prompt.library?.name || 'Unknown'}\n`;
1918
+ text += `**Length:** ${lengthChars} characters, ${lines} line(s)\n`;
1919
+ text += `**Variables:** ${foundVars.length ? foundVars.join(', ') : 'None'}\n`;
1920
+ text += `**Has role guidance:** ${mentionsRole ? 'Yes' : 'No'}\n`;
1921
+ text += `**Has output format guidance:** ${mentionsOutput ? 'Yes' : 'No'}\n`;
1922
+ text += `**Pass:** ${pass}\n`;
1923
+ text += focusNote;
1924
+
1925
+ if (improvementAreas.length) {
1926
+ text += `\n**Suggested Improvement Areas:**\n`;
1927
+ improvementAreas.forEach((area) => {
1928
+ text += `- ${area}\n`;
1929
+ });
1930
+ } else {
1931
+ text += `\n✅ No obvious structural issues detected. Focus on domain-specific refinements.\n`;
1932
+ }
1933
+
1934
+ text += `\n**Interview Questions to Ask the User:**\n`;
1935
+ interviewQuestions.forEach((q, i) => {
1936
+ text += `${i + 1}. ${q}\n`;
1937
+ });
1938
+
1939
+ text += `\n**Next Steps (for the agent):**\n`;
1940
+ text += `- Ask the user some of the questions above.\n`;
1941
+ text += `- Use the answers to propose changes via quick_iterate_prompt (updating content, description, and tags).\n`;
1942
+ text += `- Optionally use rename_prompt to align key and display name if needed.\n`;
1943
+
1944
+ const metrics = {
1945
+ prompt: {
1946
+ key: prompt.key,
1947
+ name: prompt.name,
1948
+ library: prompt.library?.name || null,
1949
+ },
1950
+ metrics: {
1951
+ lengthChars,
1952
+ lines,
1953
+ variables: foundVars,
1954
+ hasRoleGuidance: mentionsRole,
1955
+ hasOutputGuidance: mentionsOutput,
1956
+ },
1957
+ };
1958
+
1959
+ let deep = null;
1960
+ if (pass === 'deep') {
1961
+ // Very lightweight scoring per dimension; all heuristic.
1962
+ const structureIssues = [];
1963
+ if (!mentionsRole) structureIssues.push('missing_role');
1964
+ if (!mentionsOutput) structureIssues.push('missing_output');
1965
+ if (lengthChars < 300) structureIssues.push('too_short');
1966
+
1967
+ const structureScore = 100
1968
+ - (structureIssues.includes('missing_role') ? 20 : 0)
1969
+ - (structureIssues.includes('missing_output') ? 20 : 0)
1970
+ - (structureIssues.includes('too_short') ? 10 : 0);
1971
+
1972
+ const variableScore = (() => {
1973
+ if (!foundVars.length) return 60;
1974
+ let score = 90;
1975
+ if (!description.toLowerCase().includes('variable')) score -= 10;
1976
+ return score;
1977
+ })();
1978
+
1979
+ const edgeMention = /edge[- ]case|failure|error|invalid|fallback/i.test(content);
1980
+ const edgeCasesScore = edgeMention ? 80 : 65;
1981
+
1982
+ const reusabilityScore = (() => {
1983
+ let score = 80;
1984
+ if (!foundVars.length) score -= 10;
1985
+ if (lengthChars < 300) score -= 10;
1986
+ return score;
1987
+ })();
1988
+
1989
+ deep = {
1990
+ dimensions: {
1991
+ structure: {
1992
+ score: Math.max(0, Math.min(100, structureScore)),
1993
+ findings: structureIssues,
1994
+ },
1995
+ variables: {
1996
+ score: Math.max(0, Math.min(100, variableScore)),
1997
+ findings: foundVars,
1998
+ },
1999
+ edgeCases: {
2000
+ score: Math.max(0, Math.min(100, edgeCasesScore)),
2001
+ findings: edgeMention ? ['mentions edge cases or error handling'] : ['no explicit edge-case/error-handling guidance'],
2002
+ },
2003
+ reusability: {
2004
+ score: Math.max(0, Math.min(100, reusabilityScore)),
2005
+ findings: [],
2006
+ },
2007
+ },
2008
+ };
2009
+
2010
+ text += `\n**Deep Analysis (scores are heuristic):**\n`;
2011
+ text += `- Structure: ${deep.dimensions.structure.score}/100\n`;
2012
+ text += `- Variables: ${deep.dimensions.variables.score}/100\n`;
2013
+ text += `- Edge Cases: ${deep.dimensions.edgeCases.score}/100\n`;
2014
+ text += `- Reusability: ${deep.dimensions.reusability.score}/100\n`;
2015
+ }
2016
+
2017
+ const analysisJson = {
2018
+ ...metrics,
2019
+ improvementAreas,
2020
+ interviewQuestions,
2021
+ focus: focus || null,
2022
+ pass,
2023
+ deep,
2024
+ };
2025
+
2026
+ return {
2027
+ content: [
2028
+ { type: 'text', text },
2029
+ { type: 'text', text: '```json\n' + JSON.stringify(analysisJson, null, 2) + '\n```' },
2030
+ ],
2031
+ };
2032
+ } catch (error) {
2033
+ return { content: [{ type: 'text', text: `Error improving prompt: ${error.message}` }] };
2034
+ }
2035
+ }
2036
+
2037
+ async renamePrompt(params = {}) {
2038
+ try {
2039
+ const result = await this.quickStartHandler.renamePrompt(params);
2040
+ return {
2041
+ content: [
2042
+ { type: 'text', text: `✅ ${result.message}` },
2043
+ { type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' },
2044
+ ],
2045
+ };
2046
+ } catch (error) {
2047
+ return { content: [{ type: 'text', text: `Error renaming prompt: ${error.message}` }] };
2048
+ }
2049
+ }
2050
+
2051
+ async quickCreatePrompt(params) {
2052
+ try {
2053
+ const result = await this.quickStartHandler.quickCreatePrompt(params);
2054
+ return {
2055
+ content: [
2056
+ { type: 'text', text: `✅ ${result.message}\n\nNext steps:\n- Test it: quick_test_prompt(key="${result.key}")\n- Edit it: quick_iterate_prompt(key="${result.key}")` },
2057
+ { type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' }
2058
+ ]
2059
+ };
2060
+ } catch (error) {
2061
+ return { content: [{ type: 'text', text: `Error creating prompt: ${error.message}` }] };
2062
+ }
2063
+ }
2064
+
2065
+ async quickIteratePrompt(params) {
2066
+ try {
2067
+ const result = await this.quickStartHandler.quickIteratePrompt(params);
2068
+ return {
2069
+ content: [
2070
+ { type: 'text', text: `✅ ${result.message}\n\n(Backup of previous version saved)` },
2071
+ { type: 'text', text: '```json\n' + JSON.stringify(result, null, 2) + '\n```' }
2072
+ ]
2073
+ };
2074
+ } catch (error) {
2075
+ return { content: [{ type: 'text', text: `Error updating prompt: ${error.message}` }] };
2076
+ }
2077
+ }
2078
+
2079
+ getHelp({ topic } = {}) {
2080
+ const topics = {
2081
+ create: `
2082
+ **Creating Prompts**
2083
+ 1. Use \`quick_create_prompt(name="my-prompt", content="...")\`
2084
+ 2. It will be added to "My Library" by default.
2085
+ 3. Use \`quick_test_prompt(key="my-prompt")\` to verify it.
2086
+ 4. Use \`quick_iterate_prompt(key="...", name="...", description="...", tags=[...])\` to refine content & metadata.
2087
+ `,
2088
+ publish: `
2089
+ **Publishing Prompts**
2090
+ 1. Ensure your prompts are ready in a local library.
2091
+ 2. Use \`publish_manifest_flow\` to pin to IPFS and propose to a SubDAO.
2092
+ 3. This makes them available on-chain for others to discover.
2093
+ `,
2094
+ versioning: `
2095
+ **Versioning**
2096
+ - \`quick_iterate_prompt\` automatically creates backups of the previous version.
2097
+ - Prompts are stored as files in \`~/.sage/libraries/prompts/\`, so you can use Git for version control.
2098
+ - The \`key\` is a stable identifier used to reference the prompt; \`name\` is the human-readable display name.
2099
+ - Use \`rename_prompt(key="old", newKey="new", name="New Name")\` when you truly need to change the key; otherwise, prefer updating \`name\` only.
2100
+ `,
2101
+ manifest: `
2102
+ **Manifest Structure (v2)**
2103
+
2104
+ Minimal example:
2105
+
2106
+ \`\`\`json
2107
+ {
2108
+ "version": 2,
2109
+ "library": {
2110
+ "name": "My Library",
2111
+ "description": "Optional description"
2112
+ },
2113
+ "prompts": [
2114
+ {
2115
+ "key": "hello-world",
2116
+ "name": "Hello World",
2117
+ "description": "A simple test prompt",
2118
+ "tags": ["test"],
2119
+ "cid": "Qm...optional-ipfs-cid",
2120
+ "files": ["prompts/hello-world.md"]
2121
+ }
2122
+ ]
2123
+ }
2124
+ \`\`\`
2125
+
2126
+ Use \`validate_manifest(manifest=...)\` to see detailed errors and hints if anything is missing.
2127
+ `
2128
+ };
2129
+
2130
+ if (topic && topics[topic]) {
2131
+ return { content: [{ type: 'text', text: topics[topic] }] };
2132
+ }
2133
+
2134
+ const generalHelp = `
2135
+ **Sage MCP Tools Help**
2136
+
2137
+ **Quick Start:**
2138
+ - Create: \`quick_create_prompt(name="...", content="...")\`
2139
+ - Edit: \`quick_iterate_prompt(key="...", content="...")\`
2140
+ - Test: \`quick_test_prompt(key="...", variables={...})\`
2141
+
2142
+ **Discovery:**
2143
+ - Browse: \`list_prompts()\`
2144
+ - Search: \`search_prompts(query="...")\`
2145
+ - Inspect: \`get_prompt(key="...")\`
2146
+
2147
+ **Next Steps:**
2148
+ Use \`help(topic="create")\` for more details.
2149
+ `;
2150
+ return { content: [{ type: 'text', text: generalHelp }] };
2151
+ }
2152
+
972
2153
  async listLibraries(options = {}) {
973
2154
  return this.listLibrariesHandler(options);
974
2155
  }
@@ -978,22 +2159,23 @@ class SageMCPServer {
978
2159
  if (!rows.length) {
979
2160
  return { content: [{ type: 'text', text: 'No metaprompts saved yet. Use save_metaprompt to create one.' }] };
980
2161
  }
981
- const textLines = rows.map((row, idx) => `${idx + 1}. **${row.title}** (${row.slug})\n 🕒 Updated: ${row.updatedAt}\n 🔖 Tags: ${row.tags.join(', ') || 'none'}\n 📝 ${row.summary || 'No summary provided.'}\n`).join('\n');
2162
+ const textLines = rows.map((row, idx) => `${idx + 1
2163
+ }. ** ${row.title}** (${row.slug}) \n 🕒 Updated: ${row.updatedAt} \n 🔖 Tags: ${row.tags.join(', ') || 'none'} \n 📝 ${row.summary || 'No summary provided.'} \n`).join('\n');
982
2164
  return {
983
2165
  content: [
984
- { type: 'text', text: `Metaprompts (${rows.length})\n\n${textLines}` },
985
- { type: 'json', text: JSON.stringify({ metaprompts: rows }, null, 2) }
2166
+ { type: 'text', text: `Metaprompts(${rows.length}) \n\n${textLines} ` },
2167
+ { type: 'text', text: '```json\n' + JSON.stringify({ metaprompts: rows }, null, 2) + '\n```' }
986
2168
  ]
987
2169
  };
988
2170
  }
989
2171
 
990
- startMetapromptInterview({ goal, model, interviewStyle }) {
991
- const primer = metapromptDesigner.generateInterviewPrimer({ goal, model, interviewStyle });
992
- const instructions = `Use the following guidance as your first message to the LLM interviewer:\n\n${primer}`;
2172
+ startMetapromptInterview({ goal, model, interviewStyle, additionalInstructions }) {
2173
+ const primer = metapromptDesigner.generateInterviewPrimer({ goal, model, interviewStyle, additionalInstructions });
2174
+ const instructions = `Use the following guidance as your first message to the LLM interviewer: \n\n${primer} `;
993
2175
  return {
994
2176
  content: [
995
2177
  { type: 'text', text: instructions },
996
- { type: 'json', text: JSON.stringify({ primer, suggestedUse: 'Send as system message to initiate one-question-at-a-time interview.' }, null, 2) }
2178
+ { type: 'text', text: '```json\n' + JSON.stringify({ primer, suggestedUse: 'Send as system message to initiate one - question - at - a - time interview.' }, null, 2) + '\n```' }
997
2179
  ]
998
2180
  };
999
2181
  }
@@ -1020,7 +2202,7 @@ class SageMCPServer {
1020
2202
  const agentsPath = metapromptDesigner.getAgentsNotebookPath();
1021
2203
  try {
1022
2204
  fs.mkdirSync(path.dirname(agentsPath), { recursive: true });
1023
- const entry = `\n## ${title}\n\n${body}\n`;
2205
+ const entry = `\n## ${title} \n\n${body} \n`;
1024
2206
  fs.appendFileSync(agentsPath, entry, 'utf8');
1025
2207
  extras.appendedToAgents = agentsPath;
1026
2208
  } catch (e) {
@@ -1036,14 +2218,14 @@ class SageMCPServer {
1036
2218
  ...extras,
1037
2219
  };
1038
2220
  const lines = [];
1039
- lines.push(`✅ Saved metaprompt '${title}' at ${saved.path}`);
2221
+ lines.push(`✅ Saved metaprompt '${title}' at ${saved.path} `);
1040
2222
  if (destination && destination !== saved.path) {
1041
- lines.push(`📚 Workspace prompt: ${destination}`);
2223
+ lines.push(`📚 Workspace prompt: ${destination} `);
1042
2224
  }
1043
2225
  return {
1044
2226
  content: [
1045
2227
  { type: 'text', text: lines.join('\n') },
1046
- { type: 'json', text: JSON.stringify(payload, null, 2) }
2228
+ { type: 'text', text: '```json\n' + JSON.stringify(payload, null, 2) + '\n```' }
1047
2229
  ]
1048
2230
  };
1049
2231
  }
@@ -1054,13 +2236,13 @@ class SageMCPServer {
1054
2236
  const preview = loaded.body.slice(0, 800);
1055
2237
  return {
1056
2238
  content: [
1057
- { type: 'text', text: `Loaded metaprompt '${slug}'\n\n${preview}${loaded.body.length > 800 ? '\n... (truncated)' : ''}` },
1058
- { type: 'json', text: JSON.stringify({ slug, meta: loaded.meta, body: loaded.body, path: loaded.path }, null, 2) }
2239
+ { type: 'text', text: `Loaded metaprompt '${slug}'\n\n${preview}${loaded.body.length > 800 ? '\n... (truncated)' : ''} ` },
2240
+ { type: 'text', text: '```json\n' + JSON.stringify({ slug, meta: loaded.meta, body: loaded.body, path: loaded.path }, null, 2) + '\n```' }
1059
2241
  ]
1060
2242
  };
1061
2243
  } catch (error) {
1062
2244
  return {
1063
- content: [{ type: 'text', text: `Error loading metaprompt: ${error.message}` }]
2245
+ content: [{ type: 'text', text: `Error loading metaprompt: ${error.message} ` }]
1064
2246
  };
1065
2247
  }
1066
2248
  }
@@ -1068,12 +2250,12 @@ class SageMCPServer {
1068
2250
  generateMetapromptLink({ body }) {
1069
2251
  const link = metapromptDesigner.generateChatGPTLink(body);
1070
2252
  const text = link
1071
- ? `ChatGPT link ready: ${link}`
2253
+ ? `ChatGPT link ready: ${link} `
1072
2254
  : '⚠️ Prompt too long to produce a ChatGPT launch link. Copy the prompt manually.';
1073
2255
  return {
1074
2256
  content: [
1075
2257
  { type: 'text', text },
1076
- { type: 'json', text: JSON.stringify({ provider: 'chatgpt', link }, null, 2) }
2258
+ { type: 'text', text: '```json\n' + JSON.stringify({ provider: 'chatgpt', link }, null, 2) + '\n```' }
1077
2259
  ]
1078
2260
  };
1079
2261
  }
@@ -1084,7 +2266,9 @@ class SageMCPServer {
1084
2266
  let content = '';
1085
2267
  const preferred = process.env.SAGE_IPFS_GATEWAY || '';
1086
2268
  const gateways = [
1087
- preferred ? `${preferred.replace(/\/$/, '')}/ipfs/${cid}` : null,
2269
+ preferred ? `${preferred.replace(/\/$/, '')} /ipfs/${cid} ` : null,
2270
+ `https://cloudflare-ipfs.com/ipfs/${cid}`,
2271
+ `https://gateway.pinata.cloud/ipfs/${cid}`,
1088
2272
  `https://dweb.link/ipfs/${cid}`,
1089
2273
  `https://nftstorage.link/ipfs/${cid}`,
1090
2274
  `https://ipfs.io/ipfs/${cid}`
@@ -1111,7 +2295,7 @@ class SageMCPServer {
1111
2295
  if (!content) {
1112
2296
  return { content: [{ type: 'text', text: `Error downloading prompt content: ${lastErr ? lastErr.message : 'unknown error'}` }] };
1113
2297
  }
1114
-
2298
+
1115
2299
  return {
1116
2300
  content: [
1117
2301
  {
@@ -1132,41 +2316,70 @@ class SageMCPServer {
1132
2316
  }
1133
2317
  }
1134
2318
 
1135
- async listSubDAOs({ limit = 20 }) {
2319
+ async listSubDAOs({ limit = 20 } = {}) {
1136
2320
  try {
1137
- const subDAOs = await this.getSubDAOList();
2321
+ const subDAOs = await this.getSubDAOList({ limit });
2322
+
2323
+ if (!subDAOs || subDAOs.length === 0) {
2324
+ return {
2325
+ content: [{
2326
+ type: 'text',
2327
+ text: 'No SubDAOs found.\n\n' +
2328
+ 'This may be because:\n' +
2329
+ '• The subgraph is not configured (check SUBGRAPH_URL in .env)\n' +
2330
+ '• The factory contract has no SubDAOs deployed yet\n' +
2331
+ '• Network connectivity issues with the RPC endpoint\n\n' +
2332
+ 'Configuration:\n' +
2333
+ `• SUBGRAPH_URL: ${process.env.SUBGRAPH_URL || 'not set'}\n` +
2334
+ `• SUBDAO_FACTORY_ADDRESS: ${process.env.SUBDAO_FACTORY_ADDRESS || 'not set'}\n` +
2335
+ `• RPC_URL: ${process.env.RPC_URL ? process.env.RPC_URL.substring(0, 50) + '...' : 'not set'}`
2336
+ }]
2337
+ };
2338
+ }
2339
+
1138
2340
  const limitedSubDAOs = subDAOs.slice(0, limit);
1139
-
1140
- const resultText = `Found ${limitedSubDAOs.length} SubDAOs:\n\n` +
1141
- limitedSubDAOs.map((subdao, i) =>
1142
- `${i + 1}. **${subdao.name}**\n` +
2341
+ const resultText = `Found ${limitedSubDAOs.length} SubDAO(s):\n\n` +
2342
+ limitedSubDAOs.map((subdao, i) =>
2343
+ `${i + 1}. **${subdao.name || 'Unnamed SubDAO'}**\n` +
1143
2344
  ` 📍 Address: ${subdao.address}\n` +
1144
- ` 📋 Registry: ${subdao.registryAddress}\n\n`
2345
+ ` 📋 Registry: ${subdao.registryAddress || 'N/A'}\n` +
2346
+ ` 📚 Libraries: ${subdao.registries?.length || 0}\n\n`
1145
2347
  ).join('');
1146
2348
 
1147
2349
  return {
1148
- content: [
1149
- {
1150
- type: 'text',
1151
- text: resultText || 'No SubDAOs found.'
1152
- }
1153
- ]
2350
+ content: [{
2351
+ type: 'text',
2352
+ text: resultText
2353
+ }]
1154
2354
  };
1155
2355
  } catch (error) {
2356
+ this.log?.error?.('list_subdaos_failed', {
2357
+ error: error.message,
2358
+ stack: error.stack,
2359
+ subgraphUrl: process.env.SUBGRAPH_URL,
2360
+ factoryAddress: process.env.SUBDAO_FACTORY_ADDRESS
2361
+ });
2362
+
1156
2363
  return {
1157
- content: [
1158
- {
1159
- type: 'text',
1160
- text: `Error listing SubDAOs: ${error.message}`
1161
- }
1162
- ]
2364
+ content: [{
2365
+ type: 'text',
2366
+ text: `Error listing SubDAOs: ${error.message}\n\n` +
2367
+ 'This tool requires one of the following configurations:\n' +
2368
+ '• SUBGRAPH_URL (recommended) - for fast SubDAO discovery\n' +
2369
+ '• SUBDAO_FACTORY_ADDRESS + RPC_URL - for on-chain event scanning\n\n' +
2370
+ 'Current configuration:\n' +
2371
+ `• SUBGRAPH_URL: ${process.env.SUBGRAPH_URL || '❌ not set'}\n` +
2372
+ `• SUBDAO_FACTORY_ADDRESS: ${process.env.SUBDAO_FACTORY_ADDRESS || '❌ not set'}\n` +
2373
+ `• RPC_URL: ${process.env.RPC_URL ? '✅ set' : '❌ not set'}\n\n` +
2374
+ `Debug info: ${error.stack?.split('\n').slice(0, 3).join('\n')}`
2375
+ }]
1163
2376
  };
1164
2377
  }
1165
2378
  }
1166
2379
 
1167
2380
  async listSubdaoLibraries({ subdao }) {
1168
2381
  const list = await this.getSubDAOList();
1169
- const found = list.find(x => (x.address || '').toLowerCase() === String(subdao||'').toLowerCase());
2382
+ const found = list.find(x => (x.address || '').toLowerCase() === String(subdao || '').toLowerCase());
1170
2383
  if (!found) {
1171
2384
  // Attempt to rebuild from chain/subgraph
1172
2385
  const regs = await this._buildBindingsForSubdao(subdao);
@@ -1245,8 +2458,8 @@ class SageMCPServer {
1245
2458
  }
1246
2459
  }
1247
2460
 
1248
- async publishManifestFlow({ manifest, subdao = '', description = '' }) {
1249
- return this.manifestWorkflows.publishManifestFlow({ manifest, subdao, description });
2461
+ async publishManifestFlow({ manifest, subdao = '', description = '', dry_run = false }) {
2462
+ return this.manifestWorkflows.publishManifestFlow({ manifest, subdao, description, dry_run });
1250
2463
  }
1251
2464
 
1252
2465
  // Helper methods (same as HTTP server implementation)
@@ -1257,7 +2470,7 @@ class SageMCPServer {
1257
2470
  async searchSubDAOPrompts(allPrompts, targetSubDAO = '', includeContent = false) {
1258
2471
  try {
1259
2472
  const subDAOs = await this.getSubDAOList();
1260
-
2473
+
1261
2474
  for (const subDAO of subDAOs) {
1262
2475
  if (targetSubDAO && subDAO.address.toLowerCase() !== targetSubDAO.toLowerCase()) {
1263
2476
  continue;
@@ -1289,11 +2502,11 @@ class SageMCPServer {
1289
2502
  const from = Math.max(earliest, to - WINDOW);
1290
2503
  const evsUpd = await contract.queryFilter(contract.filters.PromptUpdated(), from, to).catch(() => []);
1291
2504
  for (const e of evsUpd) {
1292
- try { const { key, cid } = e.args; latestPrompts.set(key, { cid: cid.toString() }); } catch {}
2505
+ try { const { key, cid } = e.args; latestPrompts.set(key, { cid: cid.toString() }); } catch { }
1293
2506
  }
1294
2507
  const evsAdd = await contract.queryFilter(contract.filters.PromptAdded(), from, to).catch(() => []);
1295
2508
  for (const e of evsAdd) {
1296
- try { const cid = e.args.contentCID.toString(); const key = cid; if (!latestPrompts.has(key)) latestPrompts.set(key, { cid }); } catch {}
2509
+ try { const cid = e.args.contentCID.toString(); const key = cid; if (!latestPrompts.has(key)) latestPrompts.set(key, { cid }); } catch { }
1297
2510
  }
1298
2511
  to = from - 1;
1299
2512
  }
@@ -1301,7 +2514,15 @@ class SageMCPServer {
1301
2514
  for (const [key, { cid }] of latestPrompts) {
1302
2515
  let content = '';
1303
2516
  if (includeContent && cid) {
1304
- try { const IPFSManager = require('./ipfs-manager'); const ipfs = new IPFSManager(); await ipfs.initialize(); content = await ipfs.downloadPrompt(cid); } catch {}
2517
+ try {
2518
+ const IPFSManager = require('./ipfs-manager');
2519
+ const ipfs = new IPFSManager();
2520
+ await ipfs.initialize();
2521
+ const data = await ipfs.downloadJson(cid);
2522
+ content = data?.content || data?.prompt?.content || JSON.stringify(data);
2523
+ } catch (_) {
2524
+ // Best-effort: leave content empty on failure
2525
+ }
1305
2526
  }
1306
2527
  allPrompts.push({
1307
2528
  id: `${subDAO.address}-${r.libraryId}-${key}`,
@@ -1340,7 +2561,7 @@ class SageMCPServer {
1340
2561
  hexToString(hex) {
1341
2562
  try {
1342
2563
  hex = hex.replace(/^0x/, '');
1343
-
2564
+
1344
2565
  if (hex.length > 128) {
1345
2566
  // Skip the offset (first 64 chars)
1346
2567
  // Get the length from the next 64 chars
@@ -1348,7 +2569,7 @@ class SageMCPServer {
1348
2569
  const length = parseInt(lengthHex, 16) * 2;
1349
2570
  // Get the actual content
1350
2571
  const contentHex = hex.slice(128, 128 + length);
1351
-
2572
+
1352
2573
  let result = '';
1353
2574
  for (let i = 0; i < contentHex.length; i += 2) {
1354
2575
  const byte = parseInt(contentHex.substr(i, 2), 16);
@@ -1370,7 +2591,7 @@ class SageMCPServer {
1370
2591
  if (require.main === module) {
1371
2592
  const server = new SageMCPServer();
1372
2593
  server.run().catch((error) => {
1373
- try { console.error('MCP Server error:', error); } catch(_) {}
2594
+ try { console.error('MCP Server error:', error); } catch (_) { }
1374
2595
  process.exit(1);
1375
2596
  });
1376
2597
  }