PluginProbe
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses / 4.4.8
LearnPress – WordPress LMS Plugin for Create and Sell Online Courses v4.4.8
4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.9.1 4.3.9 4.3.8 4.3.7 4.1.6.9 4.1.6.9.1 4.1.6.9.2 4.1.6.9.3 4.1.6.9.4 4.1.7 4.1.7.1 4.1.7.2 4.1.7.3 4.1.7.3.1 4.1.7.3.2 4.2.0 All 139 releases
← All changes | inc/MCP/Abilities.php +284 -105 4.3.74.4.8 View file →
@@ -2,11 +2,23 @@
2 2
3 3 namespace LearnPress\MCP;
4 4
5 5 use LearnPress\MCP\Auth\AuthContext;
6 -use LearnPress\MCP\Concerns\AbilityExecutors;
7 -use LearnPress\MCP\Concerns\AbilityHelpers;
8 -use LearnPress\MCP\Concerns\AbilitySchemas;
6 +use LearnPress\MCP\Domain\CourseTools;
7 +use LearnPress\MCP\Domain\SectionTools;
8 +use LearnPress\MCP\Domain\LessonTools;
9 +use LearnPress\MCP\Domain\QuizTools;
10 +use LearnPress\MCP\Domain\QuestionTools;
11 +use LearnPress\MCP\Domain\EnrollmentTools;
12 +use LearnPress\MCP\Schemas\CourseSchemas;
13 +use LearnPress\MCP\Schemas\SectionSchemas;
14 +use LearnPress\MCP\Schemas\LessonSchemas;
15 +use LearnPress\MCP\Schemas\QuizSchemas;
16 +use LearnPress\MCP\Schemas\QuestionSchemas;
17 +use LearnPress\MCP\Schemas\EnrollmentSchemas;
18 +use LearnPress\MCP\Support\Errors;
19 +use LearnPress\MCP\Support\Pagination;
20 +use LearnPress\MCP\Support\Schemas;
9 21 use WP_REST_Server;
10 22 use WP_REST_Request;
11 23 use WP_REST_Response;
12 24 use WP_Error;
@@ -23,12 +35,8 @@
23 35 * Execution logic, schemas, and mapping helpers are split into traits.
24 36 */
25 37 class Abilities {
26 38
27 - use AbilitySchemas;
28 - use AbilityHelpers;
29 - use AbilityExecutors;
30 -
31 39 /**
32 40 * Abilities API category slug for LearnPress abilities.
33 41 */
34 42 const CATEGORY = 'learnpress';
@@ -50,14 +58,16 @@
50 58 */
51 59 protected static $initialized = false;
52 60
53 61 /**
54 - * Initialize ability registration hooks when Abilities API exists.
62 + * Initialize ability registration hooks when the WordPress Abilities API runtime is available.
55 63 *
56 64 * @return void
57 65 */
58 66 public static function init(): void {
59 - if ( self::$initialized || ! function_exists( 'wp_register_ability' ) ) {
67 + if ( self::$initialized
68 + || ! function_exists( 'wp_register_ability' )
69 + || ! function_exists( 'wp_register_ability_category' ) ) {
60 70 return;
61 71 }
62 72
63 73 add_action( 'wp_abilities_api_categories_init', array( __CLASS__, 'register_category' ) );
@@ -129,11 +139,11 @@
129 139 self::reg(
130 140 'learnpress/get-courses',
131 141 __( 'Get Courses', 'learnpress' ),
132 142 __( 'List courses with optional filters and pagination.', 'learnpress' ),
133 - self::schema_get_courses_input(),
134 - self::schema_list_output( self::schema_course_summary() ),
135 - array( __CLASS__, 'execute_get_courses' )
143 + CourseSchemas::get_courses_input(),
144 + Pagination::list_output( CourseSchemas::course_summary() ),
145 + array( CourseTools::class, 'get_courses' )
136 146 );
137 147
138 148 self::reg(
139 149 'learnpress/get-course-details',
@@ -138,11 +148,11 @@
138 148 self::reg(
139 149 'learnpress/get-course-details',
140 150 __( 'Get Course Details', 'learnpress' ),
141 151 __( 'Get details and curriculum summary for a course.', 'learnpress' ),
142 - self::schema_required_id( 'course_id' ),
143 - self::schema_course_detail_output(),
144 - array( __CLASS__, 'execute_get_course_details' )
152 + Schemas::required_id( 'course_id' ),
153 + Schemas::object_output( 'course' ),
154 + array( CourseTools::class, 'get_course_details' )
145 155 );
146 156
147 157 self::reg(
148 158 'learnpress/list-lessons',
@@ -147,11 +157,11 @@
147 157 self::reg(
148 158 'learnpress/list-lessons',
149 159 __( 'List Lessons', 'learnpress' ),
150 160 __( 'List lessons in a course with optional filters.', 'learnpress' ),
151 - self::schema_list_lessons_input(),
152 - self::schema_list_output( self::schema_lesson_summary() ),
153 - array( __CLASS__, 'execute_list_lessons' )
161 + LessonSchemas::list_lessons_input(),
162 + Pagination::list_output( LessonSchemas::lesson_summary() ),
163 + array( LessonTools::class, 'list_lessons' )
154 164 );
155 165
156 166 self::reg(
157 167 'learnpress/get-lesson-details',
@@ -156,11 +166,11 @@
156 166 self::reg(
157 167 'learnpress/get-lesson-details',
158 168 __( 'Get Lesson Details', 'learnpress' ),
159 169 __( 'Get lesson details including content, video intro, and materials.', 'learnpress' ),
160 - self::schema_required_id( 'lesson_id' ),
161 - self::schema_lesson_detail_output(),
162 - array( __CLASS__, 'execute_get_lesson_details' )
170 + Schemas::required_id( 'lesson_id' ),
171 + Schemas::object_output( 'lesson' ),
172 + array( LessonTools::class, 'get_lesson_details' )
163 173 );
164 174
165 175 self::reg(
166 176 'learnpress/list-quizzes',
@@ -165,11 +175,11 @@
165 175 self::reg(
166 176 'learnpress/list-quizzes',
167 177 __( 'List Quizzes', 'learnpress' ),
168 178 __( 'List quizzes in a course with pagination.', 'learnpress' ),
169 - self::schema_list_quizzes_input(),
170 - self::schema_list_output( self::schema_quiz_summary() ),
171 - array( __CLASS__, 'execute_list_quizzes' )
179 + QuizSchemas::list_quizzes_input(),
180 + Pagination::list_output( QuizSchemas::quiz_summary() ),
181 + array( QuizTools::class, 'list_quizzes' )
172 182 );
173 183
174 184 self::reg(
175 185 'learnpress/get-quiz-details',
@@ -174,11 +184,11 @@
174 184 self::reg(
175 185 'learnpress/get-quiz-details',
176 186 __( 'Get Quiz Details', 'learnpress' ),
177 187 __( 'Get quiz details including duration, passing grade, and question count.', 'learnpress' ),
178 - self::schema_required_id( 'quiz_id' ),
179 - self::schema_quiz_detail_output(),
180 - array( __CLASS__, 'execute_get_quiz_details' )
188 + Schemas::required_id( 'quiz_id' ),
189 + Schemas::object_output( 'quiz' ),
190 + array( QuizTools::class, 'get_quiz_details' )
181 191 );
182 192
183 193 self::reg(
184 194 'learnpress/get-student-progress',
@@ -183,11 +193,11 @@
183 193 self::reg(
184 194 'learnpress/get-student-progress',
185 195 __( 'Get Student Progress', 'learnpress' ),
186 196 __( 'Get user progress and results for a course enrollment.', 'learnpress' ),
187 - self::schema_progress_input(),
188 - self::schema_object_output( 'progress' ),
189 - array( __CLASS__, 'execute_get_student_progress' )
197 + EnrollmentSchemas::progress_input(),
198 + Schemas::object_output( 'progress' ),
199 + array( EnrollmentTools::class, 'get_student_progress' )
190 200 );
191 201
192 202 self::reg(
193 203 'learnpress/get-enrollments',
@@ -192,15 +202,193 @@
192 202 self::reg(
193 203 'learnpress/get-enrollments',
194 204 __( 'Get Enrollments', 'learnpress' ),
195 205 __( 'List course enrollments with optional filters and pagination.', 'learnpress' ),
196 - self::schema_get_enrollments_input(),
197 - self::schema_list_output( array( 'type' => 'object' ) ),
198 - array( __CLASS__, 'execute_get_enrollments' )
206 + EnrollmentSchemas::get_enrollments_input(),
207 + Pagination::list_output( array( 'type' => 'object' ) ),
208 + array( EnrollmentTools::class, 'get_enrollments' )
199 209 );
210 +
211 + self::register_write_abilities();
200 212 }
201 213
202 214 /**
215 + * Register all Phase 2 write abilities (course, section, lesson, quiz,
216 + * quiz question, and enrollment management).
217 + *
218 + * Domain logic lives in focused `LearnPress\MCP\Domain` executors and
219 + * `LearnPress\MCP\Schemas` providers, not in this orchestration class.
220 + *
221 + * @return void
222 + */
223 + protected static function register_write_abilities(): void {
224 + // Course tools.
225 + self::reg(
226 + 'learnpress/create-course',
227 + __( 'Create Course', 'learnpress' ),
228 + __( 'Create a new LearnPress course.', 'learnpress' ),
229 + CourseSchemas::create_input(),
230 + CourseSchemas::write_output(),
231 + array( CourseTools::class, 'create_course' ),
232 + self::write_annotations()
233 + );
234 + self::reg(
235 + 'learnpress/update-course',
236 + __( 'Update Course', 'learnpress' ),
237 + __( 'Update an existing LearnPress course.', 'learnpress' ),
238 + CourseSchemas::update_input(),
239 + CourseSchemas::write_output(),
240 + array( CourseTools::class, 'update_course' ),
241 + self::write_annotations()
242 + );
243 + self::reg(
244 + 'learnpress/delete-course',
245 + __( 'Delete Course', 'learnpress' ),
246 + __( 'Move a LearnPress course to trash (reversible).', 'learnpress' ),
247 + CourseSchemas::delete_input(),
248 + CourseSchemas::delete_output(),
249 + array( CourseTools::class, 'delete_course' ),
250 + self::destructive_annotations()
251 + );
252 +
253 + // Section tools.
254 + self::reg(
255 + 'learnpress/create-section',
256 + __( 'Create Section', 'learnpress' ),
257 + __( 'Create a curriculum section in a course.', 'learnpress' ),
258 + SectionSchemas::create_input(),
259 + SectionSchemas::write_output(),
260 + array( SectionTools::class, 'create_section' ),
261 + self::write_annotations()
262 + );
263 + self::reg(
264 + 'learnpress/update-section',
265 + __( 'Update Section', 'learnpress' ),
266 + __( 'Update a curriculum section in a course.', 'learnpress' ),
267 + SectionSchemas::update_input(),
268 + SectionSchemas::write_output(),
269 + array( SectionTools::class, 'update_section' ),
270 + self::write_annotations()
271 + );
272 + self::reg(
273 + 'learnpress/delete-section',
274 + __( 'Delete Section', 'learnpress' ),
275 + __( 'Remove a section relationship while preserving its lessons/quizzes (reversible).', 'learnpress' ),
276 + SectionSchemas::delete_input(),
277 + SectionSchemas::delete_output(),
278 + array( SectionTools::class, 'delete_section' ),
279 + self::destructive_annotations()
280 + );
281 +
282 + // Lesson tools.
283 + self::reg(
284 + 'learnpress/create-lesson',
285 + __( 'Create Lesson', 'learnpress' ),
286 + __( 'Create a lesson and assign it to a course section.', 'learnpress' ),
287 + LessonSchemas::create_input(),
288 + LessonSchemas::write_output(),
289 + array( LessonTools::class, 'create_lesson' ),
290 + self::write_annotations()
291 + );
292 + self::reg(
293 + 'learnpress/update-lesson',
294 + __( 'Update Lesson', 'learnpress' ),
295 + __( 'Update an existing lesson.', 'learnpress' ),
296 + LessonSchemas::update_input(),
297 + LessonSchemas::write_output(),
298 + array( LessonTools::class, 'update_lesson' ),
299 + self::write_annotations()
300 + );
301 + self::reg(
302 + 'learnpress/delete-lesson',
303 + __( 'Delete Lesson', 'learnpress' ),
304 + __( 'Move a lesson to trash and remove it from the curriculum (reversible).', 'learnpress' ),
305 + LessonSchemas::delete_input(),
306 + LessonSchemas::delete_output(),
307 + array( LessonTools::class, 'delete_lesson' ),
308 + self::destructive_annotations()
309 + );
310 +
311 + // Quiz tools.
312 + self::reg(
313 + 'learnpress/create-quiz',
314 + __( 'Create Quiz', 'learnpress' ),
315 + __( 'Create a quiz and assign it to a course section.', 'learnpress' ),
316 + QuizSchemas::create_input(),
317 + QuizSchemas::write_output(),
318 + array( QuizTools::class, 'create_quiz' ),
319 + self::write_annotations()
320 + );
321 + self::reg(
322 + 'learnpress/update-quiz',
323 + __( 'Update Quiz', 'learnpress' ),
324 + __( 'Update an existing quiz and its settings.', 'learnpress' ),
325 + QuizSchemas::update_input(),
326 + QuizSchemas::write_output(),
327 + array( QuizTools::class, 'update_quiz' ),
328 + self::write_annotations()
329 + );
330 + self::reg(
331 + 'learnpress/delete-quiz',
332 + __( 'Delete Quiz', 'learnpress' ),
333 + __( 'Move a quiz to trash and remove it from the curriculum (reversible).', 'learnpress' ),
334 + QuizSchemas::delete_input(),
335 + QuizSchemas::delete_output(),
336 + array( QuizTools::class, 'delete_quiz' ),
337 + self::destructive_annotations()
338 + );
339 +
340 + // Quiz question tools.
341 + self::reg(
342 + 'learnpress/add-quiz-question',
343 + __( 'Add Quiz Question', 'learnpress' ),
344 + __( 'Create a question and add it to a quiz.', 'learnpress' ),
345 + QuestionSchemas::add_input(),
346 + QuestionSchemas::add_output(),
347 + array( QuestionTools::class, 'add_quiz_question' ),
348 + self::write_annotations()
349 + );
350 + self::reg(
351 + 'learnpress/update-quiz-question',
352 + __( 'Update Quiz Question', 'learnpress' ),
353 + __( 'Update a quiz question and its answers.', 'learnpress' ),
354 + QuestionSchemas::update_input(),
355 + QuestionSchemas::write_output(),
356 + array( QuestionTools::class, 'update_quiz_question' ),
357 + self::write_annotations()
358 + );
359 + self::reg(
360 + 'learnpress/delete-quiz-question',
361 + __( 'Delete Quiz Question', 'learnpress' ),
362 + __( 'Remove a question from a quiz while preserving the question post (reversible).', 'learnpress' ),
363 + QuestionSchemas::delete_input(),
364 + QuestionSchemas::delete_output(),
365 + array( QuestionTools::class, 'delete_quiz_question' ),
366 + self::destructive_annotations()
367 + );
368 +
369 + // Enrollment tools.
370 + self::reg(
371 + 'learnpress/enroll-student',
372 + __( 'Enroll Student', 'learnpress' ),
373 + __( 'Manually enroll a student in a course.', 'learnpress' ),
374 + EnrollmentSchemas::enroll_input(),
375 + EnrollmentSchemas::enroll_output(),
376 + array( EnrollmentTools::class, 'enroll_student' ),
377 + self::write_annotations()
378 + );
379 + self::reg(
380 + 'learnpress/update-enrollment',
381 + __( 'Update Enrollment', 'learnpress' ),
382 + __( 'Update enrollment status and learning result metadata.', 'learnpress' ),
383 + EnrollmentSchemas::update_input(),
384 + EnrollmentSchemas::write_output(),
385 + array( EnrollmentTools::class, 'update_enrollment' ),
386 + self::write_annotations()
387 + );
388 + }
389 +
390 + /**
203 391 * Shared permission callback for LearnPress MCP abilities.
204 392 *
205 393 * @param string $ability_name Ability ID.
206 394 * @param mixed $input Ability input.
@@ -209,9 +397,9 @@
209 397 */
210 398 public static function permission_callback( string $ability_name, $input = null ) {
211 399
212 400 if ( ! AuthContext::is_api_key_auth() ) {
213 - return self::error_missing_auth();
401 + return Errors::missing_auth();
214 402 }
215 403
216 404 $current_user_id = get_current_user_id();
217 405 $base_capability = self::get_base_capability( $ability_name, $input );
@@ -216,13 +404,13 @@
216 404 $current_user_id = get_current_user_id();
217 405 $base_capability = self::get_base_capability( $ability_name, $input );
218 406
219 407 if ( $current_user_id <= 0 ) {
220 - return self::error_missing_auth();
408 + return Errors::missing_auth();
221 409 }
222 410
223 411 if ( ! current_user_can( $base_capability ) ) {
224 - return self::error_missing_base_capability( $base_capability );
412 + return Errors::missing_capability( $base_capability );
225 413 }
226 414
227 415 $required_scope = self::get_required_scope( $ability_name, $input );
228 416 $granted_scope = AuthContext::get_permissions();
@@ -227,9 +415,9 @@
227 415 $required_scope = self::get_required_scope( $ability_name, $input );
228 416 $granted_scope = AuthContext::get_permissions();
229 417
230 418 if ( ! self::scope_allows( $granted_scope, $required_scope ) ) {
231 - return self::error_insufficient_scope( $required_scope, $granted_scope );
419 + return Errors::insufficient_scope( $required_scope, $granted_scope );
232 420 }
233 421
234 422 return true;
235 423 }
@@ -241,8 +429,11 @@
241 429 * @param string $description Description for clients.
242 430 * @param array $input_schema Input JSON schema.
243 431 * @param array $output_schema Output JSON schema.
244 432 * @param callable $execute_callback Callback that executes the ability.
433 + * @param array $annotations Optional MCP annotation overrides
434 + * (readonly, destructive, idempotent).
435 + * Read tools keep the read-only defaults.
245 436 *
246 437 * @return void
247 438 */
248 439 protected static function reg(
@@ -250,14 +441,24 @@
250 441 string $label,
251 442 string $description,
252 443 array $input_schema,
253 444 array $output_schema,
254 - $execute_callback
445 + $execute_callback,
446 + array $annotations = array()
255 447 ): void {
256 448 $permission_callback = static function ( $input = null ) use ( $name ) {
257 449 return self::permission_callback( $name, $input );
258 450 };
259 451
452 + $annotations = array_merge(
453 + array(
454 + 'readonly' => true,
455 + 'destructive' => false,
456 + 'idempotent' => true,
457 + ),
458 + $annotations
459 + );
460 +
260 461 wp_register_ability(
261 462 $name,
262 463 array(
263 464 'label' => $label,
@@ -267,13 +468,9 @@
267 468 'permission_callback' => $permission_callback,
268 469 'input_schema' => $input_schema,
269 470 'output_schema' => $output_schema,
270 471 'meta' => array(
271 - 'annotations' => array(
272 - 'readonly' => true,
273 - 'destructive' => false,
274 - 'idempotent' => true,
275 - ),
472 + 'annotations' => $annotations,
276 473 'mcp' => array(
277 474 'public' => true,
278 475 'type' => 'tool',
279 476 'required_scope' => self::get_required_scope( $name ),
@@ -284,8 +481,34 @@
284 481 );
285 482 }
286 483
287 484 /**
485 + * Annotation set for create/update write tools.
486 + *
487 + * @return array
488 + */
489 + protected static function write_annotations(): array {
490 + return array(
491 + 'readonly' => false,
492 + 'destructive' => false,
493 + 'idempotent' => false,
494 + );
495 + }
496 +
497 + /**
498 + * Annotation set for destructive (delete) write tools.
499 + *
500 + * @return array
501 + */
502 + protected static function destructive_annotations(): array {
503 + return array(
504 + 'readonly' => false,
505 + 'destructive' => true,
506 + 'idempotent' => false,
507 + );
508 + }
509 +
510 + /**
288 511 * Resolve base capability required for ability execution.
289 512 *
290 513 * @param string $ability_name Ability ID.
291 514 * @param mixed $input Ability input payload.
@@ -317,8 +540,26 @@
317 540 'learnpress/list-quizzes' => 'read',
318 541 'learnpress/get-quiz-details' => 'read',
319 542 'learnpress/get-student-progress' => 'read',
320 543 'learnpress/get-enrollments' => 'read',
544 + // Phase 2 write tools require write (or read_write) scope.
545 + 'learnpress/create-course' => 'write',
546 + 'learnpress/update-course' => 'write',
547 + 'learnpress/delete-course' => 'write',
548 + 'learnpress/create-section' => 'write',
549 + 'learnpress/update-section' => 'write',
550 + 'learnpress/delete-section' => 'write',
551 + 'learnpress/create-lesson' => 'write',
552 + 'learnpress/update-lesson' => 'write',
553 + 'learnpress/delete-lesson' => 'write',
554 + 'learnpress/create-quiz' => 'write',
555 + 'learnpress/update-quiz' => 'write',
556 + 'learnpress/delete-quiz' => 'write',
557 + 'learnpress/add-quiz-question' => 'write',
558 + 'learnpress/update-quiz-question' => 'write',
559 + 'learnpress/delete-quiz-question' => 'write',
560 + 'learnpress/enroll-student' => 'write',
561 + 'learnpress/update-enrollment' => 'write',
321 562 );
322 563
323 564 $scope = $default_scopes[ $ability_name ] ?? 'read';
324 565 $scope = apply_filters( 'learn-press/mcp/ability-required-scope', $scope, $ability_name, $input );
@@ -340,68 +581,6 @@
340 581 return true;
341 582 }
342 583
343 584 return $granted_scope === $required_scope;
344 - }
345 -
346 - /**
347 - * Error for missing/invalid authentication.
348 - *
349 - * @param string $message Optional custom error message.
350 - *
351 - * @return WP_Error
352 - */
353 - protected static function error_missing_auth( string $message = '' ): WP_Error {
354 -
355 - if ( '' === $message ) {
356 - $message = __( 'Missing or invalid MCP authentication.', 'learnpress' );
357 - }
358 -
359 - return new WP_Error(
360 - 'learnpress_mcp_missing_auth',
361 - $message,
362 - array( 'status' => 401 )
363 - );
364 - }
365 -
366 - /**
367 - * Error for base capability failure.
368 - *
369 - * @param string $capability Required capability name.
370 - *
371 - * @return WP_Error
372 - */
373 - protected static function error_missing_base_capability( string $capability ): WP_Error {
374 -
375 - return new WP_Error(
376 - 'learnpress_mcp_missing_base_capability',
377 - sprintf(
378 - /* translators: %s: capability. */
379 - __( 'Current user does not have required base capability: %s.', 'learnpress' ),
380 - $capability
381 - ),
382 - array( 'status' => 403 )
383 - );
384 - }
385 -
386 - /**
387 - * Error for scope mismatch.
388 - *
389 - * @param string $required_scope Required scope for the ability.
390 - * @param string $granted_scope Scope granted by authenticated API key.
391 - *
392 - * @return WP_Error
393 - */
394 - protected static function error_insufficient_scope( string $required_scope, string $granted_scope ): WP_Error {
395 -
396 - return new WP_Error(
397 - 'learnpress_mcp_insufficient_scope',
398 - sprintf(
399 - /* translators: 1: required scope, 2: granted scope. */
400 - __( 'API key scope is insufficient. Required: %1$s. Granted: %2$s.', 'learnpress' ),
401 - $required_scope,
402 - $granted_scope
403 - ),
404 - array( 'status' => 403 )
405 - );
406 585 }
407 586 }