> ## Documentation Index
> Fetch the complete documentation index at: https://docs.konstantly.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Tools Reference

> All 25 MCP tools — parameters, permissions, example prompts, and what they return

This page documents every tool the MCP server exposes. Each tool is filtered at registration time against your key's scopes and your user's permissions — so the AI sees only what it can actually invoke.

If your tool list looks shorter than what's documented here, that's intentional: you don't have the permission or license tier required. See the [Permissions matrix](/api-reference/mcp/permissions) for which role unlocks which tool.

***

## Read tools

Available to any key with `read` scope (which is all keys).

### find\_user

Search for users by name or email substring. Case-insensitive.

<ResponseField name="query" type="string" required>
  Name or email substring to search for.
</ResponseField>

<ResponseField name="limit" type="integer">
  Max results (default 20, max 50).
</ResponseField>

**Permission**: `user_view`

**Example prompt**: *"Find a user named Sarah."*

**Backend**: `GET /api/v2/users?query=...&limit=...`

***

### get\_user

Get full details for one user by ID — name, email, role, group memberships, status.

<ResponseField name="userId" type="integer" required>
  Internal user ID (from `find_user` or other tool).
</ResponseField>

**Permission**: `user_view`

**Example prompt**: *"Show me everything about user 42."*

**Backend**: `GET /api/v2/users/:userId`

***

### find\_course

Search for courses by name. Returns matching courses (id, name, status, annotation).

<ResponseField name="query" type="string" required>
  Course name substring.
</ResponseField>

<ResponseField name="limit" type="integer">
  Max results (default 20, max 50).
</ResponseField>

**Permission**: `course_view`

**Example prompt**: *"Find the OSHA refresher course."*

**Backend**: `GET /api/v2/courses?query=...&limit=...`

***

### get\_course

Full details for one course — name, structure, settings, publish state.

<ResponseField name="courseId" type="integer" required>
  Internal course ID (from `find_course`).
</ResponseField>

**Permission**: `course_view`

**Example prompt**: *"Show me the structure of course 17."*

**Backend**: `GET /api/v2/courses/:courseId`

***

### list\_assignments

List course assignments for **one user or one group** (exactly one required). No top-level "all assignments" view — too much data; the v2 API scopes by user or group.

<ResponseField name="userId" type="integer">
  Filter by one user. Mutually exclusive with `groupId`.
</ResponseField>

<ResponseField name="groupId" type="integer">
  Filter by one group. Mutually exclusive with `userId`.
</ResponseField>

**Permission**: `user_assign`

**Example prompts**:

* *"What is Jamie working on?"* → uses `userId`
* *"What courses is the Engineering group assigned?"* → uses `groupId`

**Backend**: `GET /api/v2/users/:userId/courses/assignments` or `GET /api/v2/groups/:groupId/courses/assignments`

***

### list\_my\_assignments

Courses assigned to the current user (the key's owner), with deadlines and completion status. Useful for "what training do I owe?" questions.

No parameters.

**Permission**: any role (learner+)

**Example prompt**: *"What training do I have due this week?"*

**Backend**: `GET /api/v2/courses/assigned/me`

***

### get\_compliance\_status

The flagship audit tool. Returns compliance breakdown for one course: who's assigned, who's done, who's overdue, completion percentages. Optionally scoped to one group.

<ResponseField name="courseId" type="integer" required>
  Course to check compliance against (e.g. OSHA refresher).
</ResponseField>

<ResponseField name="groupId" type="integer">
  Optional: scope to one group of users.
</ResponseField>

**Permission**: `common_stat`

**Example prompts**:

* *"Pull a compliance report for OSHA confined-spaces training across all warehouse staff."*
* *"Are we ready for the SOC 2 audit on security training?"*

**Backend**: `GET /api/v2/courses/:courseId/statistics/users?groupId=...`

***

### list\_certificates

All certificates earned by one user, with issue dates and (when set) expiry dates. Useful for recertification planning.

<ResponseField name="userId" type="integer" required>
  User whose certificates to list.
</ResponseField>

**Permission**: `user_view`

**Example prompt**: *"Show Sarah's certifications and what's expiring in the next 90 days."*

**Backend**: `GET /api/v2/users/:userId/certificates`

***

### get\_statistics

High-level analytics — overall engagement, publishing rate, course-creation activity, insights, tag breakdowns.

<ResponseField name="scope" type="string" required>
  Which surface to query. One of: `overview`, `publishing`, `courseCreation`, `activity`, `insights`, `tags`.
</ResponseField>

**Permission**: `common_stat`

**Example prompt**: *"How are we doing on training engagement this quarter?"*

**Backend**: `GET /api/v2/statistics/{scope}`

For per-course or per-user numbers prefer `get_compliance_status` or `list_assignments`.

***

### get\_leaderboard

Top learners by points/completion this period.

<ResponseField name="limit" type="integer">
  Top N learners (default 10, max 50).
</ResponseField>

**Permission**: `common_stat`

**Example prompt**: *"Who are our top 5 learners this week? I want to send them a thank-you."*

**Backend**: `GET /api/v2/statistics/leaders?limit=...`

***

### get\_audit\_log

Read the platform audit log with optional filters. Use for security investigations, compliance questions, and inspecting actions taken by other MCP agents.

<ResponseField name="authorUserId" type="integer">
  Filter by acting user.
</ResponseField>

<ResponseField name="fromDate" type="string">
  ISO date or YYYY-MM-DD — start of window.
</ResponseField>

<ResponseField name="toDate" type="string">
  ISO date or YYYY-MM-DD — end of window.
</ResponseField>

<ResponseField name="limit" type="integer">
  Max results (default 50, max 250).
</ResponseField>

<ResponseField name="offset" type="integer">
  Pagination offset.
</ResponseField>

**Permission**: `audit_view`

**Example prompt**: *"Pull all admin actions taken last Tuesday."*

**Backend**: `GET /api/v2/audit?fields[author]=...&fields[createdAt][from]=...&fields[createdAt][to]=...`

***

## Write tools

Available when the key has `write` scope AND your license tier is `full` (Business or Enterprise).

### assign\_training

Assign one course to one learner. Idempotent — re-assigning the same user is a no-op. For bulk operations use `bulk_assign` instead.

<ResponseField name="courseId" type="integer" required>
  Course to assign.
</ResponseField>

<ResponseField name="userId" type="integer" required>
  Learner to assign to.
</ResponseField>

<ResponseField name="deadlineAt" type="integer">
  Unix timestamp (seconds) when the assignment is due. Omit for no deadline.
</ResponseField>

<ResponseField name="assignAt" type="integer">
  Unix timestamp for scheduled future assignment. Omit to assign immediately.
</ResponseField>

**Permission**: `user_assign`

**Example prompts**:

* *"Assign the new-hire onboarding to user 42, deadline two weeks from now."*
* *"Schedule the Q1 compliance refresher for the new sales rep, starting next Monday."*

**Backend**: `POST /api/v2/courses/:courseId/assignments/users`

***

### bulk\_assign

Assign multiple courses to a whole group at once. Use for onboarding bundles or compliance pushes.

<ResponseField name="groupId" type="integer" required>
  Group whose members get the assignments.
</ResponseField>

<ResponseField name="courseIds" type="integer[]" required>
  One or more courses to assign.
</ResponseField>

<ResponseField name="deadlineAt" type="integer">
  Unix-timestamp deadline applied to ALL assignments.
</ResponseField>

**Permission**: `group_assign`

**Example prompts**:

* *"Assign all new-hire training to the Engineering group, due in 30 days."*
* *"Roll out the safety refresher to all warehouse staff."*

**Backend**: per-course `POST /api/v2/courses/:courseId/assignments` with `{groups: [groupId]}`

***

### assign\_chain

Assign an entire course chain (an ordered learning path) to users or groups. The caller must own the chain (per-chain ownership rule).

<ResponseField name="chainId" type="integer" required>
  Course-chain ID.
</ResponseField>

<ResponseField name="userIds" type="integer[]">
  Users to assign the chain to.
</ResponseField>

<ResponseField name="groupIds" type="integer[]">
  Groups to assign the chain to.
</ResponseField>

<ResponseField name="deadlineAt" type="integer">
  Unix-timestamp deadline for the whole chain.
</ResponseField>

**Permission**: `user_assign` + chain ownership

**Example prompt**: *"Put the new-hire learning path on Marco's plate, due 45 days from start date."*

**Backend**: `POST /api/v2/courseChains/:chainId/assign`

***

### ban\_user

Ban a user (revokes their login + course access, preserves history). Use for offboarding. Target must be below the caller's role in the hierarchy.

<ResponseField name="userId" type="integer" required>
  User to ban.
</ResponseField>

**Permission**: `user_ban` + role hierarchy

**Example prompt**: *"Sarah left the company last Friday — disable her account."*

**Backend**: `POST /api/v2/users/banned`

***

### unban\_user

Restore a previously banned user's access. Used for reinstating offboarded learners (rehires, contractors who returned, etc.).

<ResponseField name="userId" type="integer" required>
  User to unban.
</ResponseField>

**Permission**: `user_ban` + role hierarchy

**Example prompt**: *"Marco's back from sabbatical — re-enable his account."*

**Backend**: `DELETE /api/v2/users/banned/:userId`

***

### change\_user\_email

Update a user's email address (e.g. after a name change or domain migration). Triggers the standard email-change notification.

<ResponseField name="userId" type="integer" required>
  User whose email is changing.
</ResponseField>

<ResponseField name="email" type="string" required>
  New email address.
</ResponseField>

**Permission**: `user_edit` + role hierarchy

**Example prompt**: *"Maria changed her name — update her email to [maria.gomez@acme.com](mailto:maria.gomez@acme.com)."*

**Backend**: `POST /api/v2/users/:userId/email/change`

***

### clone\_course

Duplicate an existing course (creates an unpublished copy that you can then edit). Useful as the first step when forking a course for a different audience or location.

<ResponseField name="courseId" type="integer" required>
  Course to clone.
</ResponseField>

**Permission**: `course_edit`

**Example prompt**: *"Clone the Forklift Safety course for the new Phoenix warehouse — I'll customize it for their layout."*

**Backend**: `POST /api/v2/courses/clone` with `{courseId}` in body

***

## AI course generation

Four tools that let an agent build a complete course from a prompt or from uploaded source materials. Available on **Business and Enterprise** (`mcpAccess: "full"` + `mcpAiAccess: true`). Subject to a separate, stricter quota — 5 calls per key per day, 50 per tenant per day.

The typical workflow:

1. `upload_source_material` — once per file. Returns a `sessionId`.
2. `create_course_from_files` (with the sessionId) or `create_course_from_prompt` (no files). Returns a `jobId`.
3. `get_course_generation_status` — poll every 3–5 seconds until status is `completed`, then read `result.courseId`.

### upload\_source\_material

Read a local file off the user's machine and upload it as a course source. Text is extracted and embedded immediately. PDF / DOC / DOCX / PNG / JPG / GIF / WebP, 20 MB max per file.

<ResponseField name="filePath" type="string" required>
  Absolute path to a local file on the user's machine.
</ResponseField>

<ResponseField name="sessionId" type="string">
  Optional. Reuse the same sessionId across multiple uploads to bundle files into one course-generation context. Omit and the tool generates one.
</ResponseField>

**Permission**: `course_edit` + license `mcpAiAccess`

**Example prompt**: *"Use this OSHA confined-spaces PDF I just shared as the source for a new course."*

**Returns**: `{ sessionId, filename, chunkCount, message }`

**Backend**: `POST /api/v2/chat/source-materials/upload` (multipart) then `POST /api/v2/chat/source-materials/process`

***

### create\_course\_from\_prompt

Generate a complete course (title, modules, lessons, quiz) from a plain-English description. Returns a `jobId` — the actual generation runs in the background and takes 10–30 seconds.

<ResponseField name="prompt" type="string" required>
  Plain-English description of the course. Be specific: include the topic, audience, scope.
</ResponseField>

<ResponseField name="chapters" type="integer">
  Optional. Target number of modules. Default 6.
</ResponseField>

<ResponseField name="language" type="string">
  Optional. ISO 639-1 code (`en`, `es`, `fr`, …). If omitted, the model uses the language of the prompt.
</ResponseField>

<ResponseField name="title" type="string">
  Optional. Override the AI-generated title.
</ResponseField>

**Permission**: `course_edit` + license `mcpAiAccess`

**Example prompt**: *"Build a compliance course on confined-space safety for warehouse staff. 6 lessons + a final quiz."*

**Returns**: `{ jobId, status, message }` — `status` is always `queued` here; poll `get_course_generation_status`.

**Backend**: `POST /api/v2/ai/courses/generate` with `{ prompt, options }`

***

### create\_course\_from\_files

Same as `create_course_from_prompt` but grounded in previously-uploaded source materials. The agent must call `upload_source_material` at least once in this conversation before calling this tool.

<ResponseField name="prompt" type="string" required>
  Plain-English direction for the course. The uploaded files are the primary source; the prompt sets tone, audience, scope.
</ResponseField>

<ResponseField name="sessionId" type="string" required>
  The sessionId returned by `upload_source_material`.
</ResponseField>

<ResponseField name="chapters" type="integer">
  Optional. Target number of modules.
</ResponseField>

<ResponseField name="language" type="string">
  Optional. ISO 639-1 code.
</ResponseField>

<ResponseField name="title" type="string">
  Optional. Override the title.
</ResponseField>

**Permission**: `course_edit` + license `mcpAiAccess`

**Example prompt**: *"Build a new-hire safety training based on these PDFs. 6 lessons, plain language, 10-question final quiz."*

**Backend**: `POST /api/v2/ai/courses/generate` with `{ prompt, sessionId, options }`

***

### get\_course\_generation\_status

Poll a queued generation job. Call every 3–5 seconds until `status` is `completed` (success — `result.courseId` is set) or `failed` (read `error.message`).

<ResponseField name="jobId" type="string" required>
  The `jobId` returned by `create_course_from_prompt` or `create_course_from_files`.
</ResponseField>

**Permission**: write scope (read-scope keys can't enqueue a job they're allowed to poll)

**Returns**:

```json theme={null}
{
  "jobId": "ai-course-2381",
  "status": "queued" | "running" | "completed" | "failed" | "unknown",
  "progress": 0–100,
  "result": { "courseId": 2419 },        // only when status="completed"
  "error":  { "message": "...", "attemptsMade": 1 } // only when status="failed"
}
```

**Backend**: `GET /api/v2/ai/courses/generate/:jobId`

***

## AI course refinement

Three additional MCP tools that operate on courses you (or the agent) already generated. Same license tier as the generation tools (**Business and Enterprise**) and they share the same daily quota — 5 calls per key per day, 50 per tenant per day. Each call counts as one.

The agent uses these three tools in addition to `create_course_from_prompt` / `create_course_from_files` to iterate on a course rather than starting fresh. Pick the narrowest tool for the user's intent: `regenerate_question` for one bad question, `regenerate_lesson` to rethink a lesson's brief, `refine_course` to add new structure to the whole course.

### refine\_course

Append new elements to an existing course. **Append-only by design** — this tool cannot delete or modify anything that already exists, only add. Use when the author says "add a section on X" or "add more practice" or "tack a survey at the end".

<ResponseField name="courseId" type="integer" required>
  The course to extend. Get this from `find_course` or `get_course`.
</ResponseField>

<ResponseField name="feedback" type="string" required>
  Plain-English description of what to add. Examples: *"Add a section on emergency procedures"*, *"Add two more practice quizzes after lesson 3"*, *"Add a final survey for course feedback"*.
</ResponseField>

**Permission**: `course_edit` on the specific course (not just the role alias — the user must actually own / share / administer this course).

**Returns**: `{ courseId, elementsAdded, transitionsAdded, message }`.

**Example prompt**: *"For course 248, add a section on confined-space rescue procedures after the current safety lesson — 2 lessons + a quiz."*

**Backend**: `POST /api/v2/ai/courses/:courseId/refine` with `{ feedback }`

***

### regenerate\_question

Replace a single quiz question's text and options. The new question stays on the **same concept** as the old one (we're not drifting to a new subject — just rewriting). Previous content is preserved in your workspace audit log.

<ResponseField name="questionId" type="integer" required>
  The question to regenerate. Single-choice questions only.
</ResponseField>

<ResponseField name="feedback" type="string">
  Optional. What's wrong with the current question and how to improve it. Examples: *"Make it harder"*, *"The distractors are too obvious"*, *"Reword for clarity"*. Omit for a generic "clearer version".
</ResponseField>

**Permission**: `course_edit` on the parent course.

**Returns**: `{ questionId, text, options, message }` with the new question content.

**Example prompt**: *"Question #5821 in course 248 has confusing wording — make it clearer."*

**Backend**: `POST /api/v2/ai/questions/:questionId/regenerate` with `{ feedback? }`

***

### regenerate\_lesson

Regenerate the AI-authored brief that drives the lesson's content. **Safe by construction** — this does NOT touch any pages the author has already created; it only updates the brief that the editor uses to bootstrap new content.

<ResponseField name="lessonId" type="integer" required>
  The lesson to regenerate the brief for.
</ResponseField>

<ResponseField name="feedback" type="string">
  Optional. What's wrong with the current brief and how to improve it. Examples: *"Add a section on the regulatory context"*, *"Reduce to 3 key points"*, *"Target supervisors, not learners"*. Omit for a generic clarity pass.
</ResponseField>

**Permission**: `course_edit` on the parent course.

**Returns**: `{ lessonId, brief }` with the new brief content (topic, title, description, keyPoints, targetAudience).

**Example prompt**: *"Lesson 312's brief is too theoretical — make it practical with concrete examples."*

**Backend**: `POST /api/v2/ai/lessons/:lessonId/regenerate` with `{ feedback? }`

***

## Coming soon (not yet shipped)

### extend\_deadline + send\_reminder *(preview)*

Push an assignment's deadline back, or nudge overdue learners. Currently not shipped because no corresponding v2 backend endpoint exists.

## Error handling

Every tool maps backend errors to LLM-readable strings:

| Backend status | Error shown to LLM                                                      |
| -------------- | ----------------------------------------------------------------------- |
| `403`          | *"Permission denied — your user does not have rights for this action."* |
| `404`          | *"Not found — check the ID exists."*                                    |
| `422`          | *"Invalid arguments: {message}"*                                        |
| `429`          | *"Rate limit exceeded. Slow down and retry."*                           |
| Other          | The original error message                                              |

The MCP server automatically retries 429s up to 3 times with backoff per the `Retry-After` header. Network errors, 5xx, and connection resets are propagated as `Error: ...` for the LLM to surface to you.

## Adding more tools

The tool catalog is intentionally focused — we ship what's used, not what's listable. If you'd find a specific tool valuable (e.g. "add to group", "set custom attribute", "trigger webhook"), the v2 REST API already exposes the endpoint; we just need to add a wrapper. [Get in touch](https://konstantly.com?contact=true).
