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 +280 -103 4.4.14.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';
@@ -131,11 +139,11 @@
131 139 self::reg(
132 140 'learnpress/get-courses',
133 141 __( 'Get Courses', 'learnpress' ),
134 142 __( 'List courses with optional filters and pagination.', 'learnpress' ),
135 - self::schema_get_courses_input(),
136 - self::schema_list_output( self::schema_course_summary() ),
137 - array( __CLASS__, 'execute_get_courses' )
143 + CourseSchemas::get_courses_input(),
144 + Pagination::list_output( CourseSchemas::course_summary() ),
145 + array( CourseTools::class, 'get_courses' )
138 146 );
139 147
140 148 self::reg(
141 149 'learnpress/get-course-details',
@@ -140,11 +148,11 @@
140 148 self::reg(
141 149 'learnpress/get-course-details',
142 150 __( 'Get Course Details', 'learnpress' ),
143 151 __( 'Get details and curriculum summary for a course.', 'learnpress' ),
144 - self::schema_required_id( 'course_id' ),
145 - self::schema_course_detail_output(),
146 - array( __CLASS__, 'execute_get_course_details' )
152 + Schemas::required_id( 'course_id' ),
153 + Schemas::object_output( 'course' ),
154 + array( CourseTools::class, 'get_course_details' )
147 155 );
148 156
149 157 self::reg(
150 158 'learnpress/list-lessons',
@@ -149,11 +157,11 @@
149 157 self::reg(
150 158 'learnpress/list-lessons',
151 159 __( 'List Lessons', 'learnpress' ),
152 160 __( 'List lessons in a course with optional filters.', 'learnpress' ),
153 - self::schema_list_lessons_input(),
154 - self::schema_list_output( self::schema_lesson_summary() ),
155 - array( __CLASS__, 'execute_list_lessons' )
161 + LessonSchemas::list_lessons_input(),
162 + Pagination::list_output( LessonSchemas::lesson_summary() ),
163 + array( LessonTools::class, 'list_lessons' )
156 164 );
157 165
158 166 self::reg(
159 167 'learnpress/get-lesson-details',
@@ -158,11 +166,11 @@
158 166 self::reg(
159 167 'learnpress/get-lesson-details',
160 168 __( 'Get Lesson Details', 'learnpress' ),
161 169 __( 'Get lesson details including content, video intro, and materials.', 'learnpress' ),
162 - self::schema_required_id( 'lesson_id' ),
163 - self::schema_lesson_detail_output(),
164 - array( __CLASS__, 'execute_get_lesson_details' )
170 + Schemas::required_id( 'lesson_id' ),
171 + Schemas::object_output( 'lesson' ),
172 + array( LessonTools::class, 'get_lesson_details' )
165 173 );
166 174
167 175 self::reg(
168 176 'learnpress/list-quizzes',
@@ -167,11 +175,11 @@
167 175 self::reg(
168 176 'learnpress/list-quizzes',
169 177 __( 'List Quizzes', 'learnpress' ),
170 178 __( 'List quizzes in a course with pagination.', 'learnpress' ),
171 - self::schema_list_quizzes_input(),
172 - self::schema_list_output( self::schema_quiz_summary() ),
173 - array( __CLASS__, 'execute_list_quizzes' )
179 + QuizSchemas::list_quizzes_input(),
180 + Pagination::list_output( QuizSchemas::quiz_summary() ),
181 + array( QuizTools::class, 'list_quizzes' )
174 182 );
175 183
176 184 self::reg(
177 185 'learnpress/get-quiz-details',
@@ -176,11 +184,11 @@
176 184 self::reg(
177 185 'learnpress/get-quiz-details',
178 186 __( 'Get Quiz Details', 'learnpress' ),
179 187 __( 'Get quiz details including duration, passing grade, and question count.', 'learnpress' ),
180 - self::schema_required_id( 'quiz_id' ),
181 - self::schema_quiz_detail_output(),
182 - array( __CLASS__, 'execute_get_quiz_details' )
188 + Schemas::required_id( 'quiz_id' ),
189 + Schemas::object_output( 'quiz' ),
190 + array( QuizTools::class, 'get_quiz_details' )
183 191 );
184 192
185 193 self::reg(
186 194 'learnpress/get-student-progress',
@@ -185,11 +193,11 @@
185 193 self::reg(
186 194 'learnpress/get-student-progress',
187 195 __( 'Get Student Progress', 'learnpress' ),
188 196 __( 'Get user progress and results for a course enrollment.', 'learnpress' ),
189 - self::schema_progress_input(),
190 - self::schema_object_output( 'progress' ),
191 - array( __CLASS__, 'execute_get_student_progress' )
197 + EnrollmentSchemas::progress_input(),
198 + Schemas::object_output( 'progress' ),
199 + array( EnrollmentTools::class, 'get_student_progress' )
192 200 );
193 201
194 202 self::reg(
195 203 'learnpress/get-enrollments',
@@ -194,15 +202,193 @@
194 202 self::reg(
195 203 'learnpress/get-enrollments',
196 204 __( 'Get Enrollments', 'learnpress' ),
197 205 __( 'List course enrollments with optional filters and pagination.', 'learnpress' ),
198 - self::schema_get_enrollments_input(),
199 - self::schema_list_output( array( 'type' => 'object' ) ),
200 - array( __CLASS__, 'execute_get_enrollments' )
206 + EnrollmentSchemas::get_enrollments_input(),
207 + Pagination::list_output( array( 'type' => 'object' ) ),
208 + array( EnrollmentTools::class, 'get_enrollments' )
201 209 );
210 +
211 + self::register_write_abilities();
202 212 }
203 213
204 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 + /**
205 391 * Shared permission callback for LearnPress MCP abilities.
206 392 *
207 393 * @param string $ability_name Ability ID.
208 394 * @param mixed $input Ability input.
@@ -211,9 +397,9 @@
211 397 */
212 398 public static function permission_callback( string $ability_name, $input = null ) {
213 399
214 400 if ( ! AuthContext::is_api_key_auth() ) {
215 - return self::error_missing_auth();
401 + return Errors::missing_auth();
216 402 }
217 403
218 404 $current_user_id = get_current_user_id();
219 405 $base_capability = self::get_base_capability( $ability_name, $input );
@@ -218,13 +404,13 @@
218 404 $current_user_id = get_current_user_id();
219 405 $base_capability = self::get_base_capability( $ability_name, $input );
220 406
221 407 if ( $current_user_id <= 0 ) {
222 - return self::error_missing_auth();
408 + return Errors::missing_auth();
223 409 }
224 410
225 411 if ( ! current_user_can( $base_capability ) ) {
226 - return self::error_missing_base_capability( $base_capability );
412 + return Errors::missing_capability( $base_capability );
227 413 }
228 414
229 415 $required_scope = self::get_required_scope( $ability_name, $input );
230 416 $granted_scope = AuthContext::get_permissions();
@@ -229,9 +415,9 @@
229 415 $required_scope = self::get_required_scope( $ability_name, $input );
230 416 $granted_scope = AuthContext::get_permissions();
231 417
232 418 if ( ! self::scope_allows( $granted_scope, $required_scope ) ) {
233 - return self::error_insufficient_scope( $required_scope, $granted_scope );
419 + return Errors::insufficient_scope( $required_scope, $granted_scope );
234 420 }
235 421
236 422 return true;
237 423 }
@@ -243,8 +429,11 @@
243 429 * @param string $description Description for clients.
244 430 * @param array $input_schema Input JSON schema.
245 431 * @param array $output_schema Output JSON schema.
246 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.
247 436 *
248 437 * @return void
249 438 */
250 439 protected static function reg(
@@ -252,14 +441,24 @@
252 441 string $label,
253 442 string $description,
254 443 array $input_schema,
255 444 array $output_schema,
256 - $execute_callback
445 + $execute_callback,
446 + array $annotations = array()
257 447 ): void {
258 448 $permission_callback = static function ( $input = null ) use ( $name ) {
259 449 return self::permission_callback( $name, $input );
260 450 };
261 451
452 + $annotations = array_merge(
453 + array(
454 + 'readonly' => true,
455 + 'destructive' => false,
456 + 'idempotent' => true,
457 + ),
458 + $annotations
459 + );
460 +
262 461 wp_register_ability(
263 462 $name,
264 463 array(
265 464 'label' => $label,
@@ -269,13 +468,9 @@
269 468 'permission_callback' => $permission_callback,
270 469 'input_schema' => $input_schema,
271 470 'output_schema' => $output_schema,
272 471 'meta' => array(
273 - 'annotations' => array(
274 - 'readonly' => true,
275 - 'destructive' => false,
276 - 'idempotent' => true,
277 - ),
472 + 'annotations' => $annotations,
278 473 'mcp' => array(
279 474 'public' => true,
280 475 'type' => 'tool',
281 476 'required_scope' => self::get_required_scope( $name ),
@@ -286,8 +481,34 @@
286 481 );
287 482 }
288 483
289 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 + /**
290 511 * Resolve base capability required for ability execution.
291 512 *
292 513 * @param string $ability_name Ability ID.
293 514 * @param mixed $input Ability input payload.
@@ -319,8 +540,26 @@
319 540 'learnpress/list-quizzes' => 'read',
320 541 'learnpress/get-quiz-details' => 'read',
321 542 'learnpress/get-student-progress' => 'read',
322 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',
323 562 );
324 563
325 564 $scope = $default_scopes[ $ability_name ] ?? 'read';
326 565 $scope = apply_filters( 'learn-press/mcp/ability-required-scope', $scope, $ability_name, $input );
@@ -342,68 +581,6 @@
342 581 return true;
343 582 }
344 583
345 584 return $granted_scope === $required_scope;
346 - }
347 -
348 - /**
349 - * Error for missing/invalid authentication.
350 - *
351 - * @param string $message Optional custom error message.
352 - *
353 - * @return WP_Error
354 - */
355 - protected static function error_missing_auth( string $message = '' ): WP_Error {
356 -
357 - if ( '' === $message ) {
358 - $message = __( 'Missing or invalid MCP authentication.', 'learnpress' );
359 - }
360 -
361 - return new WP_Error(
362 - 'learnpress_mcp_missing_auth',
363 - $message,
364 - array( 'status' => 401 )
365 - );
366 - }
367 -
368 - /**
369 - * Error for base capability failure.
370 - *
371 - * @param string $capability Required capability name.
372 - *
373 - * @return WP_Error
374 - */
375 - protected static function error_missing_base_capability( string $capability ): WP_Error {
376 -
377 - return new WP_Error(
378 - 'learnpress_mcp_missing_base_capability',
379 - sprintf(
380 - /* translators: %s: capability. */
381 - __( 'Current user does not have required base capability: %s.', 'learnpress' ),
382 - $capability
383 - ),
384 - array( 'status' => 403 )
385 - );
386 - }
387 -
388 - /**
389 - * Error for scope mismatch.
390 - *
391 - * @param string $required_scope Required scope for the ability.
392 - * @param string $granted_scope Scope granted by authenticated API key.
393 - *
394 - * @return WP_Error
395 - */
396 - protected static function error_insufficient_scope( string $required_scope, string $granted_scope ): WP_Error {
397 -
398 - return new WP_Error(
399 - 'learnpress_mcp_insufficient_scope',
400 - sprintf(
401 - /* translators: 1: required scope, 2: granted scope. */
402 - __( 'API key scope is insufficient. Required: %1$s. Granted: %2$s.', 'learnpress' ),
403 - $required_scope,
404 - $granted_scope
405 - ),
406 - array( 'status' => 403 )
407 - );
408 585 }
409 586 }