@mindstone-engineering/mcp-server-talentlms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +24 -0
- package/dist/auth.js +70 -0
- package/dist/bridge.d.ts +16 -0
- package/dist/bridge.js +43 -0
- package/dist/client.d.ts +17 -0
- package/dist/client.js +79 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +31 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +17 -0
- package/dist/tools/assessments.d.ts +3 -0
- package/dist/tools/assessments.js +47 -0
- package/dist/tools/branches.d.ts +3 -0
- package/dist/tools/branches.js +15 -0
- package/dist/tools/configure.d.ts +3 -0
- package/dist/tools/configure.js +63 -0
- package/dist/tools/courses.d.ts +3 -0
- package/dist/tools/courses.js +126 -0
- package/dist/tools/groups.d.ts +3 -0
- package/dist/tools/groups.js +59 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/reporting.d.ts +3 -0
- package/dist/tools/reporting.js +42 -0
- package/dist/tools/users.d.ts +3 -0
- package/dist/tools/users.js +102 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.js +12 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +42 -0
- package/package.json +48 -0
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TalentLMS authentication module.
|
|
3
|
+
*
|
|
4
|
+
* API key + domain management — stored via env vars (TALENTLMS_API_KEY, TALENTLMS_DOMAIN)
|
|
5
|
+
* or configured at runtime via the configure_talentlms tool.
|
|
6
|
+
*
|
|
7
|
+
* Auth: Basic auth with base64(apiKey:) — colon after key, empty password.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Normalize a user-provided domain input to a subdomain.
|
|
11
|
+
* Handles full URLs like "https://acme.talentlms.com" → "acme",
|
|
12
|
+
* bare domains like "acme.talentlms.com" → "acme", and plain subdomains.
|
|
13
|
+
*/
|
|
14
|
+
export declare const normalizeTalentLmsSubdomainInput: (input: string | undefined) => string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Validate that a subdomain value is safe to use in a URL.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isValidSubdomain(value: string): boolean;
|
|
19
|
+
export declare function getApiKey(): string;
|
|
20
|
+
export declare function setApiKey(key: string): void;
|
|
21
|
+
export declare function getDomain(): string;
|
|
22
|
+
export declare function setDomain(d: string): void;
|
|
23
|
+
export declare function isConfigured(): boolean;
|
|
24
|
+
//# sourceMappingURL=auth.d.ts.map
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TalentLMS authentication module.
|
|
3
|
+
*
|
|
4
|
+
* API key + domain management — stored via env vars (TALENTLMS_API_KEY, TALENTLMS_DOMAIN)
|
|
5
|
+
* or configured at runtime via the configure_talentlms tool.
|
|
6
|
+
*
|
|
7
|
+
* Auth: Basic auth with base64(apiKey:) — colon after key, empty password.
|
|
8
|
+
*/
|
|
9
|
+
const MULTI_LABEL_SUBDOMAIN_REGEX = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
10
|
+
const extractHostnameFromUserInput = (input) => {
|
|
11
|
+
const trimmed = input.trim();
|
|
12
|
+
if (!trimmed)
|
|
13
|
+
return '';
|
|
14
|
+
const candidate = trimmed.includes('://') ? trimmed : `https://${trimmed}`;
|
|
15
|
+
try {
|
|
16
|
+
return new URL(candidate).hostname.toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return trimmed
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/^[a-z]+:\/\//, '')
|
|
22
|
+
.split('/')[0]
|
|
23
|
+
.split('?')[0]
|
|
24
|
+
.split('#')[0]
|
|
25
|
+
.split(':')[0];
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Normalize a user-provided domain input to a subdomain.
|
|
30
|
+
* Handles full URLs like "https://acme.talentlms.com" → "acme",
|
|
31
|
+
* bare domains like "acme.talentlms.com" → "acme", and plain subdomains.
|
|
32
|
+
*/
|
|
33
|
+
export const normalizeTalentLmsSubdomainInput = (input) => {
|
|
34
|
+
if (!input)
|
|
35
|
+
return undefined;
|
|
36
|
+
const hostname = extractHostnameFromUserInput(input);
|
|
37
|
+
if (!hostname)
|
|
38
|
+
return undefined;
|
|
39
|
+
const normalizedHostname = hostname.trim().toLowerCase().replace(/\.$/, '');
|
|
40
|
+
const withoutSuffix = normalizedHostname.endsWith('.talentlms.com')
|
|
41
|
+
? normalizedHostname.slice(0, -'.talentlms.com'.length)
|
|
42
|
+
: normalizedHostname;
|
|
43
|
+
return withoutSuffix || undefined;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Validate that a subdomain value is safe to use in a URL.
|
|
47
|
+
*/
|
|
48
|
+
export function isValidSubdomain(value) {
|
|
49
|
+
return MULTI_LABEL_SUBDOMAIN_REGEX.test(value);
|
|
50
|
+
}
|
|
51
|
+
/** Runtime API key — starts from env, can be updated via configure tool. */
|
|
52
|
+
let apiKey = process.env.TALENTLMS_API_KEY ?? '';
|
|
53
|
+
/** Runtime domain — starts from env, can be updated via configure tool. */
|
|
54
|
+
let domain = normalizeTalentLmsSubdomainInput(process.env.TALENTLMS_DOMAIN) ?? '';
|
|
55
|
+
export function getApiKey() {
|
|
56
|
+
return apiKey;
|
|
57
|
+
}
|
|
58
|
+
export function setApiKey(key) {
|
|
59
|
+
apiKey = key;
|
|
60
|
+
}
|
|
61
|
+
export function getDomain() {
|
|
62
|
+
return domain;
|
|
63
|
+
}
|
|
64
|
+
export function setDomain(d) {
|
|
65
|
+
domain = d;
|
|
66
|
+
}
|
|
67
|
+
export function isConfigured() {
|
|
68
|
+
return apiKey.trim().length > 0 && domain.trim().length > 0;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/bridge.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path to bridge state file, supporting both current and legacy env vars.
|
|
3
|
+
*/
|
|
4
|
+
export declare const BRIDGE_STATE_PATH: string;
|
|
5
|
+
/**
|
|
6
|
+
* Send a request to the host app bridge.
|
|
7
|
+
*
|
|
8
|
+
* The bridge is an HTTP server running inside the host app (e.g. Rebel)
|
|
9
|
+
* that handles credential management and other cross-process operations.
|
|
10
|
+
*/
|
|
11
|
+
export declare const bridgeRequest: (urlPath: string, body: Record<string, unknown>) => Promise<{
|
|
12
|
+
success: boolean;
|
|
13
|
+
warning?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
}>;
|
|
16
|
+
//# sourceMappingURL=bridge.d.ts.map
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { REQUEST_TIMEOUT_MS } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Path to bridge state file, supporting both current and legacy env vars.
|
|
5
|
+
*/
|
|
6
|
+
export const BRIDGE_STATE_PATH = process.env.MCP_HOST_BRIDGE_STATE || process.env.MINDSTONE_REBEL_BRIDGE_STATE || '';
|
|
7
|
+
const loadBridgeState = () => {
|
|
8
|
+
if (!BRIDGE_STATE_PATH)
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
const raw = fs.readFileSync(BRIDGE_STATE_PATH, 'utf8');
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Send a request to the host app bridge.
|
|
20
|
+
*
|
|
21
|
+
* The bridge is an HTTP server running inside the host app (e.g. Rebel)
|
|
22
|
+
* that handles credential management and other cross-process operations.
|
|
23
|
+
*/
|
|
24
|
+
export const bridgeRequest = async (urlPath, body) => {
|
|
25
|
+
const bridge = loadBridgeState();
|
|
26
|
+
if (!bridge) {
|
|
27
|
+
return { success: false, error: 'Bridge not available' };
|
|
28
|
+
}
|
|
29
|
+
const response = await fetch(`http://127.0.0.1:${bridge.port}${urlPath}`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
Authorization: `Bearer ${bridge.token}`,
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
});
|
|
38
|
+
if (response.status === 401 || response.status === 403) {
|
|
39
|
+
return { success: false, error: `Bridge returned ${response.status}: unauthorized. Check host app authentication.` };
|
|
40
|
+
}
|
|
41
|
+
return response.json();
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=bridge.js.map
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TalentLMS API HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Centralises Basic auth injection (base64(apiKey:) with colon preserved, empty password),
|
|
5
|
+
* error handling, rate-limit messaging, and timeout handling.
|
|
6
|
+
*
|
|
7
|
+
* Base URL: https://{domain}.talentlms.com/api/v1
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Make an authenticated request to the TalentLMS API.
|
|
11
|
+
*/
|
|
12
|
+
export declare function talentlmsFetch<T>(path: string, options?: RequestInit): Promise<T>;
|
|
13
|
+
/**
|
|
14
|
+
* URL-encode form parameters, omitting undefined values.
|
|
15
|
+
*/
|
|
16
|
+
export declare function formEncode(params: Record<string, string | undefined>): string;
|
|
17
|
+
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TalentLMS API HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Centralises Basic auth injection (base64(apiKey:) with colon preserved, empty password),
|
|
5
|
+
* error handling, rate-limit messaging, and timeout handling.
|
|
6
|
+
*
|
|
7
|
+
* Base URL: https://{domain}.talentlms.com/api/v1
|
|
8
|
+
*/
|
|
9
|
+
import { TalentLMSError, REQUEST_TIMEOUT_MS } from './types.js';
|
|
10
|
+
import { getApiKey, getDomain, isConfigured } from './auth.js';
|
|
11
|
+
function getBaseUrl() {
|
|
12
|
+
return `https://${getDomain()}.talentlms.com/api/v1`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Make an authenticated request to the TalentLMS API.
|
|
16
|
+
*/
|
|
17
|
+
export async function talentlmsFetch(path, options = {}) {
|
|
18
|
+
if (!isConfigured()) {
|
|
19
|
+
throw new TalentLMSError('TalentLMS not configured', 'AUTH_REQUIRED', 'Configure your TalentLMS API key and domain. Call configure_talentlms first.');
|
|
20
|
+
}
|
|
21
|
+
const apiKey = getApiKey();
|
|
22
|
+
const url = `${getBaseUrl()}${path}`;
|
|
23
|
+
const authHeader = 'Basic ' + Buffer.from(`${apiKey}:`).toString('base64');
|
|
24
|
+
const headers = {
|
|
25
|
+
Authorization: authHeader,
|
|
26
|
+
...(options.headers || {}),
|
|
27
|
+
};
|
|
28
|
+
if (options.body) {
|
|
29
|
+
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
30
|
+
}
|
|
31
|
+
let response;
|
|
32
|
+
try {
|
|
33
|
+
response = await fetch(url, {
|
|
34
|
+
...options,
|
|
35
|
+
signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
36
|
+
headers,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof Error && error.name === 'TimeoutError') {
|
|
41
|
+
throw new TalentLMSError('Request to TalentLMS API timed out', 'TIMEOUT', 'The request took too long. Try again or check if TalentLMS is available.');
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
if (response.status === 429) {
|
|
46
|
+
throw new TalentLMSError('Rate limited by TalentLMS. Please wait before retrying.', 'RATE_LIMITED', 'Please wait before retrying. TalentLMS rate limits: 2,000-10,000 calls/hour depending on plan.');
|
|
47
|
+
}
|
|
48
|
+
if (response.status === 401 || response.status === 403) {
|
|
49
|
+
let errorText;
|
|
50
|
+
try {
|
|
51
|
+
const errorBody = await response.json();
|
|
52
|
+
errorText = errorBody?.error?.message || 'Unauthorized';
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
errorText = 'Unauthorized';
|
|
56
|
+
}
|
|
57
|
+
throw new TalentLMSError(`Authentication failed: ${errorText}`, 'AUTH_FAILED', 'Re-configure with configure_talentlms. Ensure Super Admin API access is enabled.');
|
|
58
|
+
}
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
let errorText;
|
|
61
|
+
try {
|
|
62
|
+
const errorBody = await response.json();
|
|
63
|
+
errorText = errorBody?.error?.message || JSON.stringify(errorBody);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
errorText = await response.text().catch(() => 'Unknown error');
|
|
67
|
+
}
|
|
68
|
+
throw new TalentLMSError(`TalentLMS API error (${response.status}): ${errorText}`, `HTTP_${response.status}`, 'Check the request parameters and try again.');
|
|
69
|
+
}
|
|
70
|
+
return response.json();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* URL-encode form parameters, omitting undefined values.
|
|
74
|
+
*/
|
|
75
|
+
export function formEncode(params) {
|
|
76
|
+
const entries = Object.entries(params).filter(([, v]) => v !== undefined);
|
|
77
|
+
return new URLSearchParams(entries).toString();
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=client.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* TalentLMS MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive LMS integration via TalentLMS API:
|
|
6
|
+
* - Users (list, get, create, status, courses)
|
|
7
|
+
* - Courses (list, get, create, users, enrol, unenrol, SSO)
|
|
8
|
+
* - Groups (list, get, create, add course)
|
|
9
|
+
* - Branches (list)
|
|
10
|
+
* - Reporting (site info, timeline, user progress)
|
|
11
|
+
* - Assessments (test answers, survey answers, ILT sessions)
|
|
12
|
+
*
|
|
13
|
+
* Environment variables:
|
|
14
|
+
* - TALENTLMS_API_KEY: TalentLMS API key (Super Admin required)
|
|
15
|
+
* - TALENTLMS_DOMAIN: TalentLMS subdomain (e.g., "acme" for acme.talentlms.com)
|
|
16
|
+
* - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
|
|
17
|
+
* - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
|
|
18
|
+
*/
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* TalentLMS MCP Server
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive LMS integration via TalentLMS API:
|
|
6
|
+
* - Users (list, get, create, status, courses)
|
|
7
|
+
* - Courses (list, get, create, users, enrol, unenrol, SSO)
|
|
8
|
+
* - Groups (list, get, create, add course)
|
|
9
|
+
* - Branches (list)
|
|
10
|
+
* - Reporting (site info, timeline, user progress)
|
|
11
|
+
* - Assessments (test answers, survey answers, ILT sessions)
|
|
12
|
+
*
|
|
13
|
+
* Environment variables:
|
|
14
|
+
* - TALENTLMS_API_KEY: TalentLMS API key (Super Admin required)
|
|
15
|
+
* - TALENTLMS_DOMAIN: TalentLMS subdomain (e.g., "acme" for acme.talentlms.com)
|
|
16
|
+
* - MCP_HOST_BRIDGE_STATE: Path to host app bridge state file (optional)
|
|
17
|
+
* - MINDSTONE_REBEL_BRIDGE_STATE: Legacy bridge state path (optional)
|
|
18
|
+
*/
|
|
19
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
20
|
+
import { createServer } from './server.js';
|
|
21
|
+
async function main() {
|
|
22
|
+
const server = createServer();
|
|
23
|
+
const transport = new StdioServerTransport();
|
|
24
|
+
await server.connect(transport);
|
|
25
|
+
console.error('TalentLMS MCP server running on stdio');
|
|
26
|
+
}
|
|
27
|
+
main().catch((error) => {
|
|
28
|
+
console.error('Fatal error:', error);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
31
|
+
//# sourceMappingURL=index.js.map
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { registerConfigureTools, registerUserTools, registerCourseTools, registerGroupTools, registerBranchTools, registerReportingTools, registerAssessmentTools, } from './tools/index.js';
|
|
3
|
+
export function createServer() {
|
|
4
|
+
const server = new McpServer({
|
|
5
|
+
name: 'talentlms-mcp-server',
|
|
6
|
+
version: '0.1.0',
|
|
7
|
+
});
|
|
8
|
+
registerConfigureTools(server);
|
|
9
|
+
registerUserTools(server);
|
|
10
|
+
registerCourseTools(server);
|
|
11
|
+
registerGroupTools(server);
|
|
12
|
+
registerBranchTools(server);
|
|
13
|
+
registerReportingTools(server);
|
|
14
|
+
registerAssessmentTools(server);
|
|
15
|
+
return server;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { talentlmsFetch } from '../client.js';
|
|
3
|
+
import { withErrorHandling } from '../utils.js';
|
|
4
|
+
export function registerAssessmentTools(server) {
|
|
5
|
+
server.registerTool('get_talentlms_test_answers', {
|
|
6
|
+
description: 'Get a user\'s answers for a specific test/quiz.\n\n' +
|
|
7
|
+
'Returns: questions, user answers, correct answers, score.\n\n' +
|
|
8
|
+
'WORKFLOW:\n' +
|
|
9
|
+
'1. Get course details with get_talentlms_course to find test/unit IDs\n' +
|
|
10
|
+
'2. Call this tool with the test ID and user ID',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
test_id: z.string().min(1).describe('Test/quiz ID (from course units)'),
|
|
13
|
+
user_id: z.string().min(1).describe('User ID'),
|
|
14
|
+
}),
|
|
15
|
+
annotations: { readOnlyHint: true },
|
|
16
|
+
}, withErrorHandling(async (args) => {
|
|
17
|
+
const result = await talentlmsFetch(`/gettestanswers/test_id:${encodeURIComponent(args.test_id)},user_id:${encodeURIComponent(args.user_id)}`);
|
|
18
|
+
return JSON.stringify({ ok: true, testAnswers: result });
|
|
19
|
+
}));
|
|
20
|
+
server.registerTool('get_talentlms_survey_answers', {
|
|
21
|
+
description: 'Get a user\'s responses to a survey.\n\n' +
|
|
22
|
+
'Returns: questions, user responses.',
|
|
23
|
+
inputSchema: z.object({
|
|
24
|
+
survey_id: z.string().min(1).describe('Survey ID (from course units)'),
|
|
25
|
+
user_id: z.string().min(1).describe('User ID'),
|
|
26
|
+
}),
|
|
27
|
+
annotations: { readOnlyHint: true },
|
|
28
|
+
}, withErrorHandling(async (args) => {
|
|
29
|
+
const result = await talentlmsFetch(`/getsurveyanswers/survey_id:${encodeURIComponent(args.survey_id)},user_id:${encodeURIComponent(args.user_id)}`);
|
|
30
|
+
return JSON.stringify({ ok: true, surveyAnswers: result });
|
|
31
|
+
}));
|
|
32
|
+
server.registerTool('get_talentlms_ilt_sessions', {
|
|
33
|
+
description: 'Get instructor-led training (ILT) sessions for a specific ILT unit.\n\n' +
|
|
34
|
+
'Returns: session ID, course, instructor, date, time, location, enrolled users.\n\n' +
|
|
35
|
+
'WORKFLOW:\n' +
|
|
36
|
+
'1. Get course details with get_talentlms_course to find ILT unit IDs\n' +
|
|
37
|
+
'2. Call this tool with the ILT unit ID',
|
|
38
|
+
inputSchema: z.object({
|
|
39
|
+
ilt_id: z.string().min(1).describe('ILT unit ID (from course units)'),
|
|
40
|
+
}),
|
|
41
|
+
annotations: { readOnlyHint: true },
|
|
42
|
+
}, withErrorHandling(async (args) => {
|
|
43
|
+
const sessions = await talentlmsFetch(`/getiltsessions/ilt_id:${encodeURIComponent(args.ilt_id)}`);
|
|
44
|
+
return JSON.stringify({ ok: true, sessions, count: sessions.length });
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=assessments.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { talentlmsFetch } from '../client.js';
|
|
3
|
+
import { withErrorHandling } from '../utils.js';
|
|
4
|
+
export function registerBranchTools(server) {
|
|
5
|
+
server.registerTool('list_talentlms_branches', {
|
|
6
|
+
description: 'List all branches in TalentLMS (multi-tenant).\n\n' +
|
|
7
|
+
'Returns: id, name, description, created_on.',
|
|
8
|
+
inputSchema: z.object({}),
|
|
9
|
+
annotations: { readOnlyHint: true },
|
|
10
|
+
}, withErrorHandling(async () => {
|
|
11
|
+
const branches = await talentlmsFetch('/branches');
|
|
12
|
+
return JSON.stringify({ ok: true, branches, count: branches.length });
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=branches.js.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { setApiKey, setDomain, normalizeTalentLmsSubdomainInput, isValidSubdomain } from '../auth.js';
|
|
3
|
+
import { bridgeRequest, BRIDGE_STATE_PATH } from '../bridge.js';
|
|
4
|
+
import { TalentLMSError } from '../types.js';
|
|
5
|
+
import { withErrorHandling } from '../utils.js';
|
|
6
|
+
export function registerConfigureTools(server) {
|
|
7
|
+
server.registerTool('configure_talentlms', {
|
|
8
|
+
description: 'Configure TalentLMS API credentials. Call this when the user provides their API key and domain.\n\n' +
|
|
9
|
+
'WORKFLOW:\n' +
|
|
10
|
+
'1. Go to your TalentLMS admin panel → Account & Settings → Security\n' +
|
|
11
|
+
'2. Enable API access\n' +
|
|
12
|
+
'3. Copy the API key\n' +
|
|
13
|
+
'4. Your domain is the subdomain part of your URL (e.g., "acme" for acme.talentlms.com)\n\n' +
|
|
14
|
+
'Note: Requires a paid TalentLMS plan and Super Admin access.',
|
|
15
|
+
inputSchema: z.object({
|
|
16
|
+
api_key: z.string().min(1).describe('TalentLMS API key'),
|
|
17
|
+
domain: z.string().min(1).describe('TalentLMS subdomain (e.g., "acme" for acme.talentlms.com)'),
|
|
18
|
+
}),
|
|
19
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
20
|
+
}, withErrorHandling(async (args) => {
|
|
21
|
+
const apiKey = args.api_key.trim();
|
|
22
|
+
const domain = normalizeTalentLmsSubdomainInput(args.domain);
|
|
23
|
+
if (!apiKey || !domain) {
|
|
24
|
+
return JSON.stringify({ ok: false, error: 'Both api_key and domain are required.' });
|
|
25
|
+
}
|
|
26
|
+
if (!isValidSubdomain(domain)) {
|
|
27
|
+
return JSON.stringify({
|
|
28
|
+
ok: false,
|
|
29
|
+
error: 'Invalid domain. Enter just the subdomain part (e.g., "acme" or "acme.eu"), or paste your TalentLMS URL (e.g., https://acme.talentlms.com).',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// If bridge is available, persist via bridge
|
|
33
|
+
if (BRIDGE_STATE_PATH) {
|
|
34
|
+
try {
|
|
35
|
+
const result = await bridgeRequest('/bundled/talentlms/configure', { apiKey, domain });
|
|
36
|
+
if (result.success) {
|
|
37
|
+
setApiKey(apiKey);
|
|
38
|
+
setDomain(domain);
|
|
39
|
+
const message = result.warning
|
|
40
|
+
? `TalentLMS configured successfully. Note: ${result.warning}`
|
|
41
|
+
: `TalentLMS configured successfully for ${domain}.talentlms.com! Try list_talentlms_users or list_talentlms_courses.`;
|
|
42
|
+
return JSON.stringify({ ok: true, message });
|
|
43
|
+
}
|
|
44
|
+
// Bridge returned failure — surface as error, do NOT fall through
|
|
45
|
+
throw new TalentLMSError(result.error || 'Bridge configuration failed', 'BRIDGE_ERROR', 'The host app bridge rejected the configuration request. Check the host app logs.');
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof TalentLMSError)
|
|
49
|
+
throw error;
|
|
50
|
+
// Bridge request failed (network, timeout, etc.) — surface as error
|
|
51
|
+
throw new TalentLMSError(`Bridge request failed: ${error instanceof Error ? error.message : String(error)}`, 'BRIDGE_ERROR', 'Could not reach the host app bridge. Ensure the host app is running.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// No bridge — store in-memory
|
|
55
|
+
setApiKey(apiKey);
|
|
56
|
+
setDomain(domain);
|
|
57
|
+
return JSON.stringify({
|
|
58
|
+
ok: true,
|
|
59
|
+
message: `TalentLMS configured successfully for ${domain}.talentlms.com! Try list_talentlms_users or list_talentlms_courses.`,
|
|
60
|
+
});
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=configure.js.map
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { talentlmsFetch, formEncode } from '../client.js';
|
|
3
|
+
import { withErrorHandling } from '../utils.js';
|
|
4
|
+
export function registerCourseTools(server) {
|
|
5
|
+
server.registerTool('list_talentlms_courses', {
|
|
6
|
+
description: 'List all courses in TalentLMS.\n\n' +
|
|
7
|
+
'Returns: id, name, code, category_id, description, status, creation_date, price, creator_id.\n\n' +
|
|
8
|
+
'RELATED TOOLS:\n' +
|
|
9
|
+
'- get_talentlms_course: Get full course details\n' +
|
|
10
|
+
'- get_talentlms_course_users: See enrolled users',
|
|
11
|
+
inputSchema: z.object({}),
|
|
12
|
+
annotations: { readOnlyHint: true },
|
|
13
|
+
}, withErrorHandling(async () => {
|
|
14
|
+
const courses = await talentlmsFetch('/courses');
|
|
15
|
+
const compact = courses.map(c => ({
|
|
16
|
+
id: c.id, name: c.name, code: c.code, category_id: c.category_id,
|
|
17
|
+
description: c.description, status: c.status, creation_date: c.creation_date,
|
|
18
|
+
price: c.price, creator_id: c.creator_id,
|
|
19
|
+
}));
|
|
20
|
+
return JSON.stringify({ ok: true, courses: compact, count: compact.length });
|
|
21
|
+
}));
|
|
22
|
+
server.registerTool('get_talentlms_course', {
|
|
23
|
+
description: 'Get full course details by ID.\n\n' +
|
|
24
|
+
'Returns: name, description, units (content structure), rules, prerequisites, certification, custom fields.\n\n' +
|
|
25
|
+
'RELATED TOOLS:\n' +
|
|
26
|
+
'- list_talentlms_courses: Find course IDs\n' +
|
|
27
|
+
'- get_talentlms_course_users: See who is enrolled',
|
|
28
|
+
inputSchema: z.object({
|
|
29
|
+
course_id: z.string().min(1).describe('Course ID'),
|
|
30
|
+
}),
|
|
31
|
+
annotations: { readOnlyHint: true },
|
|
32
|
+
}, withErrorHandling(async (args) => {
|
|
33
|
+
const course = await talentlmsFetch(`/courses/id:${encodeURIComponent(args.course_id)}`);
|
|
34
|
+
return JSON.stringify({ ok: true, course });
|
|
35
|
+
}));
|
|
36
|
+
server.registerTool('create_talentlms_course', {
|
|
37
|
+
description: 'Create a new course in TalentLMS.\n\n' +
|
|
38
|
+
'RELATED TOOLS:\n' +
|
|
39
|
+
'- enrol_talentlms_user: Add users to the new course',
|
|
40
|
+
inputSchema: z.object({
|
|
41
|
+
name: z.string().min(1).describe('Course name'),
|
|
42
|
+
description: z.string().optional().describe('Course description'),
|
|
43
|
+
code: z.string().optional().describe('Course code (optional)'),
|
|
44
|
+
category_id: z.string().optional().describe('Category ID (optional)'),
|
|
45
|
+
creator_id: z.string().optional().describe('Creator user ID (optional)'),
|
|
46
|
+
}),
|
|
47
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
48
|
+
}, withErrorHandling(async (args) => {
|
|
49
|
+
const body = formEncode({
|
|
50
|
+
name: args.name,
|
|
51
|
+
description: args.description,
|
|
52
|
+
code: args.code,
|
|
53
|
+
category_id: args.category_id,
|
|
54
|
+
creator_id: args.creator_id,
|
|
55
|
+
});
|
|
56
|
+
const course = await talentlmsFetch('/createcourse', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
body,
|
|
59
|
+
});
|
|
60
|
+
return JSON.stringify({ ok: true, message: 'Course created.', course });
|
|
61
|
+
}));
|
|
62
|
+
server.registerTool('get_talentlms_course_users', {
|
|
63
|
+
description: 'Get all users enrolled in a course, with their progress and completion status.\n\n' +
|
|
64
|
+
'Returns: user id, name, role, completion_status, completion_percentage, total_time.\n\n' +
|
|
65
|
+
'RELATED TOOLS:\n' +
|
|
66
|
+
'- get_talentlms_user_courses: See all courses for a user (reverse lookup)\n' +
|
|
67
|
+
'- enrol_talentlms_user: Add a user to this course',
|
|
68
|
+
inputSchema: z.object({
|
|
69
|
+
course_id: z.string().min(1).describe('Course ID'),
|
|
70
|
+
}),
|
|
71
|
+
annotations: { readOnlyHint: true },
|
|
72
|
+
}, withErrorHandling(async (args) => {
|
|
73
|
+
const course = await talentlmsFetch(`/courses/id:${encodeURIComponent(args.course_id)}`);
|
|
74
|
+
const users = course.users || [];
|
|
75
|
+
return JSON.stringify({ ok: true, users, count: users.length });
|
|
76
|
+
}));
|
|
77
|
+
server.registerTool('enrol_talentlms_user', {
|
|
78
|
+
description: 'Enrol a user into a course.\n\n' +
|
|
79
|
+
'COMMON MISTAKES:\n' +
|
|
80
|
+
'- User must exist first (use create_talentlms_user if needed)\n' +
|
|
81
|
+
'- Cannot enrol the same user twice',
|
|
82
|
+
inputSchema: z.object({
|
|
83
|
+
user_id: z.string().min(1).describe('User ID'),
|
|
84
|
+
course_id: z.string().min(1).describe('Course ID'),
|
|
85
|
+
role: z.enum(['learner', 'instructor']).optional().describe('Enrolment role. Default: learner'),
|
|
86
|
+
}),
|
|
87
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
88
|
+
}, withErrorHandling(async (args) => {
|
|
89
|
+
const body = formEncode({
|
|
90
|
+
user_id: args.user_id,
|
|
91
|
+
course_id: args.course_id,
|
|
92
|
+
role: args.role,
|
|
93
|
+
});
|
|
94
|
+
await talentlmsFetch('/addusertocourse', {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
body,
|
|
97
|
+
});
|
|
98
|
+
return JSON.stringify({ ok: true, message: 'User enrolled in course.' });
|
|
99
|
+
}));
|
|
100
|
+
server.registerTool('unenrol_talentlms_user', {
|
|
101
|
+
description: 'Remove a user from a course.\n\n' +
|
|
102
|
+
'RELATED TOOLS:\n' +
|
|
103
|
+
'- get_talentlms_course_users: Check current enrolment',
|
|
104
|
+
inputSchema: z.object({
|
|
105
|
+
user_id: z.string().min(1).describe('User ID'),
|
|
106
|
+
course_id: z.string().min(1).describe('Course ID'),
|
|
107
|
+
}),
|
|
108
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
109
|
+
}, withErrorHandling(async (args) => {
|
|
110
|
+
await talentlmsFetch(`/removeuserfromcourse/course_id:${encodeURIComponent(args.course_id)},user_id:${encodeURIComponent(args.user_id)}`);
|
|
111
|
+
return JSON.stringify({ ok: true, message: 'User removed from course.' });
|
|
112
|
+
}));
|
|
113
|
+
server.registerTool('get_talentlms_course_sso_link', {
|
|
114
|
+
description: 'Generate an SSO link to launch a user directly into a course.\n\n' +
|
|
115
|
+
'Returns a URL that logs the user in and redirects them to the course. Link is temporary.',
|
|
116
|
+
inputSchema: z.object({
|
|
117
|
+
user_id: z.string().min(1).describe('User ID'),
|
|
118
|
+
course_id: z.string().min(1).describe('Course ID'),
|
|
119
|
+
}),
|
|
120
|
+
annotations: { readOnlyHint: true },
|
|
121
|
+
}, withErrorHandling(async (args) => {
|
|
122
|
+
const result = await talentlmsFetch(`/gotocourse/user_id:${encodeURIComponent(args.user_id)},course_id:${encodeURIComponent(args.course_id)}`);
|
|
123
|
+
return JSON.stringify({ ok: true, result });
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=courses.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { talentlmsFetch, formEncode } from '../client.js';
|
|
3
|
+
import { withErrorHandling } from '../utils.js';
|
|
4
|
+
export function registerGroupTools(server) {
|
|
5
|
+
server.registerTool('list_talentlms_groups', {
|
|
6
|
+
description: 'List all groups in TalentLMS.\n\n' +
|
|
7
|
+
'Returns: id, name, description, creator_id, created_on, key (enrollment key).\n\n' +
|
|
8
|
+
'RELATED TOOLS:\n' +
|
|
9
|
+
'- get_talentlms_group: Get group details including members and courses',
|
|
10
|
+
inputSchema: z.object({}),
|
|
11
|
+
annotations: { readOnlyHint: true },
|
|
12
|
+
}, withErrorHandling(async () => {
|
|
13
|
+
const groups = await talentlmsFetch('/groups');
|
|
14
|
+
return JSON.stringify({ ok: true, groups, count: groups.length });
|
|
15
|
+
}));
|
|
16
|
+
server.registerTool('get_talentlms_group', {
|
|
17
|
+
description: 'Get group details including members and assigned courses.\n\n' +
|
|
18
|
+
'Returns: group info, list of users in the group, list of courses assigned to the group.',
|
|
19
|
+
inputSchema: z.object({
|
|
20
|
+
group_id: z.string().min(1).describe('Group ID'),
|
|
21
|
+
}),
|
|
22
|
+
annotations: { readOnlyHint: true },
|
|
23
|
+
}, withErrorHandling(async (args) => {
|
|
24
|
+
const group = await talentlmsFetch(`/groups/id:${encodeURIComponent(args.group_id)}`);
|
|
25
|
+
return JSON.stringify({ ok: true, group });
|
|
26
|
+
}));
|
|
27
|
+
server.registerTool('create_talentlms_group', {
|
|
28
|
+
description: 'Create a new group in TalentLMS.',
|
|
29
|
+
inputSchema: z.object({
|
|
30
|
+
name: z.string().min(1).describe('Group name'),
|
|
31
|
+
description: z.string().optional().describe('Group description'),
|
|
32
|
+
key: z.string().optional().describe('Enrollment key (optional)'),
|
|
33
|
+
}),
|
|
34
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
35
|
+
}, withErrorHandling(async (args) => {
|
|
36
|
+
const body = formEncode({
|
|
37
|
+
name: args.name,
|
|
38
|
+
description: args.description,
|
|
39
|
+
key: args.key,
|
|
40
|
+
});
|
|
41
|
+
const group = await talentlmsFetch('/creategroup', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
body,
|
|
44
|
+
});
|
|
45
|
+
return JSON.stringify({ ok: true, message: 'Group created.', group });
|
|
46
|
+
}));
|
|
47
|
+
server.registerTool('add_course_to_talentlms_group', {
|
|
48
|
+
description: 'Assign a course to a group. All group members will be enrolled.',
|
|
49
|
+
inputSchema: z.object({
|
|
50
|
+
group_id: z.string().min(1).describe('Group ID'),
|
|
51
|
+
course_id: z.string().min(1).describe('Course ID'),
|
|
52
|
+
}),
|
|
53
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
54
|
+
}, withErrorHandling(async (args) => {
|
|
55
|
+
await talentlmsFetch(`/addcoursetogroup/group_id:${encodeURIComponent(args.group_id)},course_id:${encodeURIComponent(args.course_id)}`);
|
|
56
|
+
return JSON.stringify({ ok: true, message: 'Course added to group.' });
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=groups.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { registerConfigureTools } from './configure.js';
|
|
2
|
+
export { registerUserTools } from './users.js';
|
|
3
|
+
export { registerCourseTools } from './courses.js';
|
|
4
|
+
export { registerGroupTools } from './groups.js';
|
|
5
|
+
export { registerBranchTools } from './branches.js';
|
|
6
|
+
export { registerReportingTools } from './reporting.js';
|
|
7
|
+
export { registerAssessmentTools } from './assessments.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { registerConfigureTools } from './configure.js';
|
|
2
|
+
export { registerUserTools } from './users.js';
|
|
3
|
+
export { registerCourseTools } from './courses.js';
|
|
4
|
+
export { registerGroupTools } from './groups.js';
|
|
5
|
+
export { registerBranchTools } from './branches.js';
|
|
6
|
+
export { registerReportingTools } from './reporting.js';
|
|
7
|
+
export { registerAssessmentTools } from './assessments.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { talentlmsFetch } from '../client.js';
|
|
3
|
+
import { withErrorHandling } from '../utils.js';
|
|
4
|
+
export function registerReportingTools(server) {
|
|
5
|
+
server.registerTool('get_talentlms_site_info', {
|
|
6
|
+
description: 'Get TalentLMS site-level statistics and configuration.\n\n' +
|
|
7
|
+
'Returns: total users, total courses, signup method, site name, timezone, domain, and more.',
|
|
8
|
+
inputSchema: z.object({}),
|
|
9
|
+
annotations: { readOnlyHint: true },
|
|
10
|
+
}, withErrorHandling(async () => {
|
|
11
|
+
const info = await talentlmsFetch('/siteinfo');
|
|
12
|
+
return JSON.stringify({ ok: true, siteInfo: info });
|
|
13
|
+
}));
|
|
14
|
+
server.registerTool('get_talentlms_timeline', {
|
|
15
|
+
description: 'Get activity timeline for users or courses.\n\n' +
|
|
16
|
+
'Returns recent activity events: enrolments, completions, logins, course accesses.',
|
|
17
|
+
inputSchema: z.object({
|
|
18
|
+
type: z.enum(['users', 'courses']).describe('Timeline type'),
|
|
19
|
+
}),
|
|
20
|
+
annotations: { readOnlyHint: true },
|
|
21
|
+
}, withErrorHandling(async (args) => {
|
|
22
|
+
const timeline = await talentlmsFetch(`/gettimeline/type:${args.type}`);
|
|
23
|
+
return JSON.stringify({ ok: true, timeline, count: timeline.length });
|
|
24
|
+
}));
|
|
25
|
+
server.registerTool('get_talentlms_user_progress', {
|
|
26
|
+
description: 'Get detailed progress for a user in a specific course.\n\n' +
|
|
27
|
+
'Returns: unit-by-unit progress, completion status, score, time spent per unit.\n\n' +
|
|
28
|
+
'WORKFLOW:\n' +
|
|
29
|
+
'1. Find user ID with list_talentlms_users\n' +
|
|
30
|
+
'2. Find course ID with list_talentlms_courses or get_talentlms_user_courses\n' +
|
|
31
|
+
'3. Call this tool for detailed breakdown',
|
|
32
|
+
inputSchema: z.object({
|
|
33
|
+
user_id: z.string().min(1).describe('User ID'),
|
|
34
|
+
course_id: z.string().min(1).describe('Course ID'),
|
|
35
|
+
}),
|
|
36
|
+
annotations: { readOnlyHint: true },
|
|
37
|
+
}, withErrorHandling(async (args) => {
|
|
38
|
+
const result = await talentlmsFetch(`/getuserstatusincourse/course_id:${encodeURIComponent(args.course_id)},user_id:${encodeURIComponent(args.user_id)}`);
|
|
39
|
+
return JSON.stringify({ ok: true, progress: result });
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=reporting.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { talentlmsFetch, formEncode } from '../client.js';
|
|
3
|
+
import { withErrorHandling } from '../utils.js';
|
|
4
|
+
export function registerUserTools(server) {
|
|
5
|
+
server.registerTool('list_talentlms_users', {
|
|
6
|
+
description: 'List all users in TalentLMS.\n\n' +
|
|
7
|
+
'Returns: id, login, first_name, last_name, email, role, status, last_updated.\n\n' +
|
|
8
|
+
'RELATED TOOLS:\n' +
|
|
9
|
+
'- get_talentlms_user: Get full profile by ID\n' +
|
|
10
|
+
'- get_talentlms_user_courses: See courses a user is enrolled in',
|
|
11
|
+
inputSchema: z.object({}),
|
|
12
|
+
annotations: { readOnlyHint: true },
|
|
13
|
+
}, withErrorHandling(async () => {
|
|
14
|
+
const users = await talentlmsFetch('/users');
|
|
15
|
+
const compact = users.map(u => ({
|
|
16
|
+
id: u.id, login: u.login, first_name: u.first_name, last_name: u.last_name,
|
|
17
|
+
email: u.email, role: u.role, status: u.status, last_updated: u.last_updated,
|
|
18
|
+
}));
|
|
19
|
+
return JSON.stringify({ ok: true, users: compact, count: compact.length });
|
|
20
|
+
}));
|
|
21
|
+
server.registerTool('get_talentlms_user', {
|
|
22
|
+
description: 'Get a user\'s full profile by ID or email.\n\n' +
|
|
23
|
+
'Returns: full profile including role, status, custom fields, last login, created date.\n\n' +
|
|
24
|
+
'RELATED TOOLS:\n' +
|
|
25
|
+
'- list_talentlms_users: Find user IDs\n' +
|
|
26
|
+
'- get_talentlms_user_courses: See their enrolled courses',
|
|
27
|
+
inputSchema: z.object({
|
|
28
|
+
user_id: z.string().optional().describe('User ID'),
|
|
29
|
+
email: z.string().optional().describe('User email (alternative to user_id)'),
|
|
30
|
+
}),
|
|
31
|
+
annotations: { readOnlyHint: true },
|
|
32
|
+
}, withErrorHandling(async (args) => {
|
|
33
|
+
const userId = args.user_id;
|
|
34
|
+
const email = args.email;
|
|
35
|
+
if (!userId && !email) {
|
|
36
|
+
return JSON.stringify({ ok: false, error: 'Provide either user_id or email.', resolution: 'Use list_talentlms_users to find user IDs.' });
|
|
37
|
+
}
|
|
38
|
+
const path = userId
|
|
39
|
+
? `/users/id:${encodeURIComponent(userId)}`
|
|
40
|
+
: `/users/email:${encodeURIComponent(email)}`;
|
|
41
|
+
const user = await talentlmsFetch(path);
|
|
42
|
+
return JSON.stringify({ ok: true, user });
|
|
43
|
+
}));
|
|
44
|
+
server.registerTool('create_talentlms_user', {
|
|
45
|
+
description: 'Create a new user in TalentLMS.\n\n' +
|
|
46
|
+
'COMMON MISTAKES:\n' +
|
|
47
|
+
'- login must be unique across TalentLMS instance\n' +
|
|
48
|
+
'- email must be unique unless allow_duplicate_emails is enabled',
|
|
49
|
+
inputSchema: z.object({
|
|
50
|
+
first_name: z.string().min(1).describe('First name'),
|
|
51
|
+
last_name: z.string().min(1).describe('Last name'),
|
|
52
|
+
email: z.string().min(1).describe('Email address'),
|
|
53
|
+
login: z.string().min(1).describe('Login username'),
|
|
54
|
+
password: z.string().optional().describe('Password (auto-generated if omitted)'),
|
|
55
|
+
user_type: z.string().optional().describe('User type: Learner, Trainer, Admin, SuperAdmin. Default: Learner'),
|
|
56
|
+
}),
|
|
57
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
58
|
+
}, withErrorHandling(async (args) => {
|
|
59
|
+
const body = formEncode({
|
|
60
|
+
first_name: args.first_name,
|
|
61
|
+
last_name: args.last_name,
|
|
62
|
+
email: args.email,
|
|
63
|
+
login: args.login,
|
|
64
|
+
password: args.password,
|
|
65
|
+
user_type: args.user_type,
|
|
66
|
+
});
|
|
67
|
+
const user = await talentlmsFetch('/usersignup', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
body,
|
|
70
|
+
});
|
|
71
|
+
return JSON.stringify({ ok: true, message: 'User created.', user });
|
|
72
|
+
}));
|
|
73
|
+
server.registerTool('set_talentlms_user_status', {
|
|
74
|
+
description: 'Activate or deactivate a user in TalentLMS.\n\n' +
|
|
75
|
+
'RELATED TOOLS:\n' +
|
|
76
|
+
'- list_talentlms_users: Find user IDs',
|
|
77
|
+
inputSchema: z.object({
|
|
78
|
+
user_id: z.string().min(1).describe('User ID'),
|
|
79
|
+
status: z.enum(['active', 'inactive']).describe('New status'),
|
|
80
|
+
}),
|
|
81
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
82
|
+
}, withErrorHandling(async (args) => {
|
|
83
|
+
const path = `/usersetstatus/user_id:${encodeURIComponent(args.user_id)},status:${args.status}`;
|
|
84
|
+
const result = await talentlmsFetch(path);
|
|
85
|
+
return JSON.stringify({ ok: true, message: `User status set to ${args.status}.`, result });
|
|
86
|
+
}));
|
|
87
|
+
server.registerTool('get_talentlms_user_courses', {
|
|
88
|
+
description: 'Get all courses a user is enrolled in, with progress and completion status.\n\n' +
|
|
89
|
+
'Returns: course id, name, role, completion status, progress percentage, total_time, last_accessed.\n\n' +
|
|
90
|
+
'RELATED TOOLS:\n' +
|
|
91
|
+
'- get_talentlms_course_users: See all users in a course (reverse lookup)',
|
|
92
|
+
inputSchema: z.object({
|
|
93
|
+
user_id: z.string().min(1).describe('User ID'),
|
|
94
|
+
}),
|
|
95
|
+
annotations: { readOnlyHint: true },
|
|
96
|
+
}, withErrorHandling(async (args) => {
|
|
97
|
+
const user = await talentlmsFetch(`/users/id:${encodeURIComponent(args.user_id)}`);
|
|
98
|
+
const courses = user.courses || [];
|
|
99
|
+
return JSON.stringify({ ok: true, courses, count: courses.length });
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=users.js.map
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const REQUEST_TIMEOUT_MS: number;
|
|
2
|
+
export interface BridgeState {
|
|
3
|
+
port: number;
|
|
4
|
+
token: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class TalentLMSError extends Error {
|
|
7
|
+
readonly code: string;
|
|
8
|
+
readonly resolution: string;
|
|
9
|
+
constructor(message: string, code: string, resolution: string);
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const REQUEST_TIMEOUT_MS = parseInt(process.env.TALENTLMS_REQUEST_TIMEOUT || '30000', 10);
|
|
2
|
+
export class TalentLMSError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
resolution;
|
|
5
|
+
constructor(message, code, resolution) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.resolution = resolution;
|
|
9
|
+
this.name = 'TalentLMSError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
type ToolHandler<T> = (args: T, extra: unknown) => Promise<CallToolResult>;
|
|
3
|
+
/**
|
|
4
|
+
* Wraps a tool handler with standard error handling.
|
|
5
|
+
*
|
|
6
|
+
* - On success: returns the string result as a text content block.
|
|
7
|
+
* - On TalentLMSError: returns a structured JSON error with code and resolution.
|
|
8
|
+
* - On unknown error: returns a generic error message.
|
|
9
|
+
*
|
|
10
|
+
* Secrets are never exposed in error messages.
|
|
11
|
+
*/
|
|
12
|
+
export declare function withErrorHandling<T>(fn: (args: T, extra: unknown) => Promise<string>): ToolHandler<T>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { TalentLMSError } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Wraps a tool handler with standard error handling.
|
|
4
|
+
*
|
|
5
|
+
* - On success: returns the string result as a text content block.
|
|
6
|
+
* - On TalentLMSError: returns a structured JSON error with code and resolution.
|
|
7
|
+
* - On unknown error: returns a generic error message.
|
|
8
|
+
*
|
|
9
|
+
* Secrets are never exposed in error messages.
|
|
10
|
+
*/
|
|
11
|
+
export function withErrorHandling(fn) {
|
|
12
|
+
return async (args, extra) => {
|
|
13
|
+
try {
|
|
14
|
+
const result = await fn(args, extra);
|
|
15
|
+
return { content: [{ type: 'text', text: result }] };
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error instanceof TalentLMSError) {
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: JSON.stringify({
|
|
24
|
+
ok: false,
|
|
25
|
+
error: error.message,
|
|
26
|
+
code: error.code,
|
|
27
|
+
resolution: error.resolution,
|
|
28
|
+
}),
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
isError: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: 'text', text: JSON.stringify({ ok: false, error: errorMessage }) }],
|
|
37
|
+
isError: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=utils.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mindstone-engineering/mcp-server-talentlms",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TalentLMS MCP server for Model Context Protocol hosts — users, courses, groups, branches, reporting, assessments",
|
|
5
|
+
"license": "FSL-1.1-MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-server-talentlms": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"!dist/**/*.map"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/nspr-io/mcp-servers.git",
|
|
17
|
+
"directory": "connectors/talentlms"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/nspr-io/mcp-servers/tree/main/connectors/talentlms",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc && shx chmod +x dist/index.js",
|
|
25
|
+
"prepare": "npm run build",
|
|
26
|
+
"watch": "tsc --watch",
|
|
27
|
+
"start": "node dist/index.js",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"test:coverage": "vitest run --coverage"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
34
|
+
"zod": "^3.23.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@mindstone-engineering/mcp-test-harness": "file:../../test-harness",
|
|
38
|
+
"@types/node": "^22",
|
|
39
|
+
"@vitest/coverage-v8": "^4.1.3",
|
|
40
|
+
"msw": "^2.13.2",
|
|
41
|
+
"shx": "^0.3.4",
|
|
42
|
+
"typescript": "^5.8.2",
|
|
43
|
+
"vitest": "^4.1.3"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=20"
|
|
47
|
+
}
|
|
48
|
+
}
|