| 1 |
<?php |
| 2 |
/** |
| 3 |
* List Code Snippets — read-only discovery ability for ZIP AI's native snippet registry. |
| 4 |
* |
| 5 |
* Exists alongside `zipai/run-snippet` (ManageCodeSnippet) specifically to give the |
| 6 |
* LLM an unambiguous, read-only entry point for answering questions like |
| 7 |
* "how many snippets do I have?" — the LLM was reliably SKIPPING the manage tool |
| 8 |
* because its id ("run-snippet") implied execution rather than listing, leading |
| 9 |
* to hallucinated "0 snippets" answers even when the filesystem had several. |
| 10 |
* |
| 11 |
* This ability is a direct read of wp-content/zip-ai-snippets/manifest.php. It does |
| 12 |
* not mutate state and requires no approval. Calling it is always safe. |
| 13 |
* |
| 14 |
* @since 0.0.5 |
| 15 |
* @package zip-ai |
| 16 |
*/ |
| 17 |
|
| 18 |
namespace ZipAI\MCP\Classes\Abilities\Zipai\System; |
| 19 |
|
| 20 |
use ZipAI\MCP\Classes\Abilities\Abstract_Ability; |
| 21 |
use ZipAI\MCP\Classes\Core\Tool_Types; |
| 22 |
use ZipAI\MCP\Classes\Core\Response; |
| 23 |
use ZipAI\MCP\Classes\Core\Snippet_Store; |
| 24 |
|
| 25 |
if ( ! defined( 'ABSPATH' ) ) { |
| 26 |
exit; |
| 27 |
} |
| 28 |
|
| 29 |
class ListCodeSnippets extends Abstract_Ability { |
| 30 |
|
| 31 |
/** |
| 32 |
* Flags this ability as non-destructive (read-only). |
| 33 |
* |
| 34 |
* @var bool |
| 35 |
*/ |
| 36 |
protected $is_destructive = false; |
| 37 |
|
| 38 |
/** |
| 39 |
* Configures the ability's id, label, description and metadata. |
| 40 |
* |
| 41 |
* @return void |
| 42 |
*/ |
| 43 |
public function configure() { |
| 44 |
$this->id = 'zipai/list-snippets'; |
| 45 |
$this->label = 'List Code Snippets'; |
| 46 |
// Factual contract only. Instructional copy ("USE THIS FIRST", |
| 47 |
// "Never report a snippet count without calling this tool first") |
| 48 |
// belongs in the server prompts, not the schema description. |
| 49 |
$this->description = 'Read-only enumeration of every snippet registered in ZIP AI\'s native registry (wp-content/zip-ai-snippets/manifest.php). ' |
| 50 |
. 'ZIP AI snippets are stored on the filesystem, NOT in any database table — DB queries (wp_snippets, wpcode posts, post_type=snippet, etc.) return zero. ' |
| 51 |
. 'Returns: { count, snippets: [{ slug, title, files, enabled, description, execution, conditions }] }. ' |
| 52 |
. 'To modify (create/update/enable/delete), use zipai/run-snippet.'; |
| 53 |
// Read surface, so it tracks the REST list route's capability rather |
| 54 |
// than the write cap — an admin who cannot author snippets still needs |
| 55 |
// to see what is running. See Snippet_Store::read_capability(). |
| 56 |
$this->capability = Snippet_Store::read_capability(); |
| 57 |
} |
| 58 |
|
| 59 |
/** |
| 60 |
* Returns the tool-type classification for this ability. |
| 61 |
* |
| 62 |
* @return string One of the Tool_Types constants. |
| 63 |
*/ |
| 64 |
public function get_tool_type() { |
| 65 |
return Tool_Types::READ; |
| 66 |
} |
| 67 |
|
| 68 |
/** |
| 69 |
* Returns the JSON Schema for this ability's input arguments. |
| 70 |
* |
| 71 |
* @return array<string,mixed> JSON Schema describing accepted arguments. |
| 72 |
*/ |
| 73 |
public function get_input_schema() { |
| 74 |
return array( |
| 75 |
'type' => 'object', |
| 76 |
'properties' => new \stdClass(), |
| 77 |
'additionalProperties' => false, |
| 78 |
); |
| 79 |
} |
| 80 |
|
| 81 |
/** |
| 82 |
* Returns the JSON Schema for this ability's response. |
| 83 |
* |
| 84 |
* @return array<string,mixed> JSON Schema describing the response shape. |
| 85 |
*/ |
| 86 |
public function get_output_schema() { |
| 87 |
return array( |
| 88 |
'type' => 'object', |
| 89 |
'properties' => array( |
| 90 |
'success' => array( 'type' => 'boolean' ), |
| 91 |
'message' => array( 'type' => 'string' ), |
| 92 |
'data' => array( |
| 93 |
'type' => 'object', |
| 94 |
'properties' => array( |
| 95 |
'count' => array( 'type' => 'integer' ), |
| 96 |
'snippets' => array( |
| 97 |
'type' => 'array', |
| 98 |
'items' => array( |
| 99 |
'type' => 'object', |
| 100 |
'properties' => array( |
| 101 |
'slug' => array( 'type' => 'string' ), |
| 102 |
'title' => array( 'type' => 'string' ), |
| 103 |
'enabled' => array( 'type' => 'boolean' ), |
| 104 |
'description' => array( 'type' => 'string' ), |
| 105 |
'files' => array( |
| 106 |
'type' => 'array', |
| 107 |
'items' => array( 'type' => 'string' ), |
| 108 |
), |
| 109 |
), |
| 110 |
), |
| 111 |
), |
| 112 |
), |
| 113 |
), |
| 114 |
), |
| 115 |
); |
| 116 |
} |
| 117 |
|
| 118 |
/** |
| 119 |
* Enumerates every snippet registered in the native registry. |
| 120 |
* |
| 121 |
* @param array<string,mixed> $input Validated input arguments (unused). |
| 122 |
* @return array<string,mixed> Standardized success response with the snippet list. |
| 123 |
*/ |
| 124 |
public function execute( $input = array() ) { |
| 125 |
$manifest = Snippet_Store::load(); |
| 126 |
$snippets = array(); |
| 127 |
|
| 128 |
foreach ( $manifest as $slug => $meta ) { |
| 129 |
if ( '_version' === $slug ) { |
| 130 |
continue; |
| 131 |
} |
| 132 |
/** |
| 133 |
* Narrowed type for `$meta_array`. |
| 134 |
* |
| 135 |
* @var array<string,mixed> $meta_array |
| 136 |
*/ |
| 137 |
$meta_array = is_array( $meta ) ? $meta : array(); |
| 138 |
$normalized = Snippet_Store::normalize_snippet( $meta_array ); |
| 139 |
$snippets[] = array( |
| 140 |
'slug' => $slug, |
| 141 |
'title' => $normalized['title'], |
| 142 |
'files' => $normalized['files'], |
| 143 |
'enabled' => (bool) $normalized['enabled'], |
| 144 |
'description' => $normalized['description'], |
| 145 |
'execution' => $normalized['execution'], |
| 146 |
'conditions' => $normalized['conditions'], |
| 147 |
); |
| 148 |
} |
| 149 |
|
| 150 |
return Response::success( |
| 151 |
count( $snippets ) . ' snippets found.', |
| 152 |
array( |
| 153 |
'count' => count( $snippets ), |
| 154 |
'snippets' => $snippets, |
| 155 |
) |
| 156 |
); |
| 157 |
} |
| 158 |
} |
| 159 |
|