PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / modules / mcp-core / Registry / ToolDescriptor.php

ToolDescriptor.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.8.0, at modules/mcp-core/Registry/ToolDescriptor.php

163 lines 5.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * A single agent capability declaration (spec 044-native-mcp-server, FR-009).
4 *
5 * This is the ONE place a capability is described. Three consumers read it:
6 * the native MCP server (always), the Abilities API bridge (only when that API
7 * exists), and the mcp-adapter bridge (only when the adapter exists). Nothing
8 * may keep its own parallel list — see ToolRegistry.
9 *
10 * ## access_level vs annotations
11 *
12 * These are two DIFFERENT fields serving two DIFFERENT purposes, and conflating
13 * them is a privilege bug:
14 *
15 * - `access_level` — the SECURITY control. Decides whether a read-only
16 * credential may invoke this capability.
17 * - `annotations` — advisory hints shown to agent clients (readonly /
18 * destructive / idempotent). NEVER consulted for access.
19 *
20 * They may legitimately disagree, and where they do the access level wins.
21 * `templately/auth-login-with-google` is the live example: it is annotated
22 * `readonly => true` (it mutates no content) yet it *initiates an account-linking
23 * flow*, so its access level is `full`. Gating on the annotation would let a
24 * read-only credential start that flow.
25 *
26 * @package Templately\Modules\McpCore\Registry
27 */
28
29 namespace Templately\Modules\McpCore\Registry;
30
31 class ToolDescriptor {
32
33 /** Read-only capabilities: invokable by any credential. */
34 const ACCESS_READ = 'read';
35
36 /** Mutating/consequential capabilities: require a full-access credential. */
37 const ACCESS_FULL = 'full';
38
39 /** @var string Stable capability id, e.g. `templately/discover-templates`. */
40 public $id;
41
42 /** @var string */
43 public $label;
44
45 /** @var string */
46 public $description;
47
48 /** @var array JSON Schema. */
49 public $input_schema;
50
51 /** @var array JSON Schema. */
52 public $output_schema;
53
54 /** @var callable */
55 public $execute_callback;
56
57 /** @var callable */
58 public $permission_callback;
59
60 /** @var string self::ACCESS_READ|self::ACCESS_FULL */
61 public $access_level;
62
63 /** @var array Advisory client hints. Not a security control. */
64 public $annotations;
65
66 private function __construct( array $args ) {
67 $this->id = $args['id'];
68 $this->label = $args['label'] ?? '';
69 $this->description = $args['description'] ?? '';
70 $this->input_schema = $args['input_schema'] ?? [ 'type' => 'object' ];
71 $this->output_schema = $args['output_schema'] ?? [ 'type' => 'object' ];
72 $this->execute_callback = $args['execute_callback'];
73 $this->permission_callback = $args['permission_callback'];
74 $this->annotations = $args['annotations'] ?? [];
75
76 // FR-026c — a capability that forgets to declare its access level is
77 // treated as requiring FULL access. Fail closed, never open: the cost of
78 // a wrong `full` is an over-restricted read-only token (visible, easily
79 // fixed); the cost of a wrong `read` is a privilege escalation (silent).
80 $level = $args['access_level'] ?? null;
81
82 $this->access_level = ( self::ACCESS_READ === $level ) ? self::ACCESS_READ : self::ACCESS_FULL;
83 }
84
85 /**
86 * @param array $args
87 * @return self
88 * @throws \InvalidArgumentException When a required field is missing.
89 */
90 public static function from_array( array $args ): self {
91 foreach ( [ 'id', 'execute_callback', 'permission_callback' ] as $required ) {
92 if ( empty( $args[ $required ] ) ) {
93 throw new \InvalidArgumentException(
94 sprintf( 'ToolDescriptor is missing required field "%s".', $required )
95 );
96 }
97 }
98
99 return new self( $args );
100 }
101
102 /**
103 * True when this capability is invokable by a read-only credential.
104 *
105 * @return bool
106 */
107 public function is_read_only(): bool {
108 return self::ACCESS_READ === $this->access_level;
109 }
110
111 /**
112 * Whether the advisory `readonly` annotation disagrees with the declared
113 * access level. Purely diagnostic — callers MUST NOT reconcile the two
114 * (FR-026b); it exists so tests and docs can assert the divergence is
115 * intentional rather than drift.
116 *
117 * @return bool
118 */
119 public function annotation_disagrees_with_access_level(): bool {
120 if ( ! array_key_exists( 'readonly', $this->annotations ) ) {
121 return false;
122 }
123
124 return (bool) $this->annotations['readonly'] !== $this->is_read_only();
125 }
126
127 /**
128 * Projection for a capability listing — the agent-facing shape.
129 * Deliberately omits callbacks and the access level.
130 *
131 * @return array
132 */
133 public function to_tool_listing(): array {
134 return [
135 'name' => $this->id,
136 'description' => $this->description,
137 'inputSchema' => $this->input_schema,
138 'annotations' => $this->annotations,
139 ];
140 }
141
142 /**
143 * Projection for `wp_register_ability()`.
144 *
145 * @return array
146 */
147 public function to_ability_args(): array {
148 return [
149 'label' => $this->label,
150 'description' => $this->description,
151 'category' => 'templately',
152 'input_schema' => $this->input_schema,
153 'output_schema' => $this->output_schema,
154 'execute_callback' => $this->execute_callback,
155 'permission_callback' => $this->permission_callback,
156 'meta' => [
157 'show_in_rest' => true,
158 'annotations' => $this->annotations,
159 ],
160 ];
161 }
162 }
163