feat(mcp-tools): ScholiqToolProvider — AI companion MCP tools - #39
Merged
Conversation
…eleton)
First per-app implementation of OCA\OpenRegister\Mcp\IMcpToolProvider for
Scholiq (LVS + LMS). Scholiq handles student data, which is privacy
sensitive — so the MVP deliberately ships ONLY the two least sensitive,
read-only tools (the course catalogue and a course's module structure):
- scholiq.listCourses — list visible courses (catalogue only, no
learner data); optional limit (1-50, capped at 20) + status filter.
- scholiq.getCourseDetails — one course by id/uuid/slug with its ordered
module (Lesson) structure; course + module metadata only — never
Enrolment, Attestation, Credential or learner objects.
Tools that touch learner records are deferred to a follow-up that wires
proper per-student authorisation (a teacher of that learner's group, the
learner themself, or an admin).
Architecture (hydra ADR-034 + ADR-035):
- lib/Mcp/ScholiqToolProvider.php delegates to OR's ObjectService (same
pattern existing scholiq controllers/handlers already use); injects
IUserSession + IGroupManager for the auth gate, LoggerInterface for
error logging.
- Tool ids namespaced scholiq.{tool}; getTools() always returns the full
catalogue; per-request authorisation runs in invokeTool() AFTER argument
validation but BEFORE business logic. requireCourseReadAccess() rejects
anonymous callers; OpenRegister RBAC inside ObjectService is the second,
per-object gate (so _rbac/_multitenancy stay at their default true).
- invokeTool() never throws — all failure paths return a structured error
array (invalid_arguments / forbidden / not_found / internal_error /
unknown_tool).
- Application.php registers the alias
'OCA\OpenRegister\Mcp\IMcpToolProvider::scholiq' → ScholiqToolProvider.
Until openregister PR #1466 (ai-chat-companion-orchestrator) ships the
real interface, Scholiq implements the stub at
tests/Stubs/Mcp/IMcpToolProvider.php (loaded by tests/bootstrap.php and
tests/bootstrap-unit.php when the interface isn't autoloadable).
Tests: tests/Unit/Mcp/ScholiqToolProviderTest.php — 8 tests / 58
assertions: getAppId, catalogue shape, unknown-tool error envelope (no
throw), argument validation, the anonymous-caller forbidden path, and a
happy-path getCourseDetails that asserts no learner-PII keys leak.
Static analysis: psalm.xml suppresses Undefined-class for the
IMcpToolProvider interface; phpstan.neon scans tests/Stubs so the
cross-app interface resolves, plus broadens the existing OpenRegister
ignore patterns ('unknown interface', 'has invalid type') — which also
clears some pre-existing CredentialIssuanceHandler/XapiCompletionHandler
noise.
Widget mount: bumps @conduction/nextcloud-vue to ^1.0.0-beta.30 (scholiq
already mounts CnAppRoot, which surfaces the AI chat companion from the
manifest's openregister dependency — no extra scaffolding needed).
Refs #36
Contributor
Quality Report — ConductionNL/scholiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ❌ | ||||
| stylelint | ❌ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 428/428 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-05-12 05:13 UTC
Download the full PDF report from the workflow artifacts.
…q-mcp-tools # Conflicts: # lib/AppInfo/Application.php # package-lock.json # package.json
Contributor
Quality Report — ConductionNL/scholiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 429/429 | |||
| PHPUnit | ❌ | ||||
| Newman | ❌ | ||||
| Playwright | ⏭️ |
Quality workflow — 2026-05-12 12:39 UTC
Download the full PDF report from the workflow artifacts.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First per-app implementation of
OCA\OpenRegister\Mcp\IMcpToolProviderfor Scholiq (LVS + LMS), per hydra ADR-034 + ADR-035. A minimal, privacy-conscious MVP skeleton — see the privacy posture below.lib/Mcp/ScholiqToolProvider.phpdelegates to OpenRegister'sObjectService(the same pattern existing Scholiq controllers/handlers already use), injectingIUserSession+IGroupManagerfor the auth gate andLoggerInterfacefor error logging.Application.phpregisters the aliasOCA\OpenRegister\Mcp\IMcpToolProvider::scholiq→ScholiqToolProviderso OR'sMcpToolsServicediscovers it.Tools (2, read-only)
scholiq.listCourses— list Scholiq courses visible to you (course catalogue only — no enrolled-learner data). Optionallimit(1–50, hard-capped at 20) andstatusfilter (draft/published/archived).scholiq.getCourseDetails— fetch one course byid/uuid/slugwith its ordered module (Lesson) structure. Returns course metadata + module metadata only — neverEnrolment,Attestation,Credentialor learner objects.Privacy posture (student data is privacy-sensitive)
Scholiq stores student records, so the MVP deliberately exposes only the two least privacy-sensitive tools — the course catalogue and a course's module structure. Neither tool returns any per-learner PII:
getCourseDetailsreturns the course + its modules; it never readsEnrolment/Attestation/Credential/learner objects. The unit test asserts nolearner/enrolment/credential/attestationkeys appear in the response.invokeTool(), after argument validation but before any data read.requireCourseReadAccess()rejects anonymous callers (no unconditionalreturn true, nocatch(\Throwable)swallowing the verdict); OpenRegister's RBAC layer insideObjectServiceis the second, per-object gate (so_rbac/_multitenancystay at their defaulttrue).invokeTool()never throws — every failure path returns a structured error array (invalid_arguments/forbidden/not_found/internal_error/unknown_tool).Student-data tools (learner progress, enrolments, attestations, credentials, compliance coverage — the rest of #36) are deferred to a follow-up that wires proper per-student authorisation: the caller must be a teacher of that student's group, the student themself, or an admin.
Stub
Until openregister PR #1466 (
ai-chat-companion-orchestrator) ships the real interface, Scholiq implements the stub attests/Stubs/Mcp/IMcpToolProvider.php(loaded bytests/bootstrap.phpandtests/bootstrap-unit.phpwhen the interface isn't autoloadable).psalm.xmlsuppresses the Undefined-class warning;phpstan.neonscanstests/Stubsso the cross-app interface resolves (and broadens the existing OpenRegister ignore patterns, which also clears some pre-existingCredentialIssuanceHandler/XapiCompletionHandlernoise).Tests & quality
tests/Unit/Mcp/ScholiqToolProviderTest.php— 8 tests / 58 assertions:getAppId() === 'scholiq', 2 descriptors withscholiq.ids + non-empty descriptions + validinputSchema,invokeTool('scholiq.bogus', [])returns an error array (no throw), argument validation, the anonymous-callerforbiddenpath, and a happy-pathgetCourseDetailsasserting no learner-PII leaks. All 8 pass in the Nextcloud container.composer phpcs— clean.composer phpmd— clean for the new file.composer phpstan— no new errors (6 remaining are pre-existing inAuditPackExportController/HealthController).composer check— ALL CHECKS PASSED.npm run build— succeeds (size warnings only).Done vs deferred
Done: the 2 read-only course tools, DI registration, stub + bootstrap wiring, unit tests, static-analysis suppressions,
@conduction/nextcloud-vuebump to^1.0.0-beta.30(scholiq already mountsCnAppRoot, which surfaces the AI chat companion from the manifest'sopenregisterdependency — no extra scaffolding needed).Deferred:
CnAppRoot, so the companion mount comes "for free" via the dep bump + manifest.Refs #36 · ADR-034 · ADR-035
MCP coverage
Adds tool: scholiq.listCourses
Adds tool: scholiq.getCourseDetails