@massteck/aloc-mcp-server 1.0.1
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/README.md +59 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.js +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +204 -0
- package/package.json +38 -0
- package/src/client.ts +73 -0
- package/src/index.ts +224 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# ALOC Model Context Protocol (MCP) Server
|
|
2
|
+
|
|
3
|
+
Official Model Context Protocol (MCP) server for **ALOC** — providing African educational examination content, curriculum structures, and AI-enriched explanations to AI agents and LLM clients (Claude Desktop, Cursor, LangChain, AI Tutors).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features & Tools
|
|
8
|
+
|
|
9
|
+
| Tool | Layer | Credits | Description |
|
|
10
|
+
| :--- | :---: | :---: | :--- |
|
|
11
|
+
| `aloc_search_questions` | L1 / L2 | 1–3 credits | Search curriculum-aligned examination questions (WAEC, JAMB, NECO) with options, topics, and difficulty. |
|
|
12
|
+
| `aloc_get_question_explanation` | L3 | 10 credits | Retrieve step-by-step solutions, misconceptions, and explanations. |
|
|
13
|
+
| `aloc_get_curriculum_taxonomy` | L2 | Free/3 credits | Explore the topic taxonomy and syllabus hierarchy. |
|
|
14
|
+
| `aloc_find_similar_questions` | L4 | 10 credits | Find vector-similar exam questions. |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
### 1. Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g @massteck/aloc-mcp-server
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. Configuration (Claude Desktop)
|
|
27
|
+
|
|
28
|
+
Add the following to your `claude_desktop_config.json`:
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"mcpServers": {
|
|
33
|
+
"aloc": {
|
|
34
|
+
"command": "npx",
|
|
35
|
+
"args": ["-y", "@massteck/aloc-mcp-server"],
|
|
36
|
+
"env": {
|
|
37
|
+
"ALOC_API_KEY": "your_aloc_api_key_here",
|
|
38
|
+
"ALOC_BASE_URL": "https://dev.aloc.com.ng/api/v1"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3. Configuration (Cursor / Agent Frameworks)
|
|
46
|
+
|
|
47
|
+
Set the environment variables:
|
|
48
|
+
* `ALOC_API_KEY`: Your ALOC Developer API Key (`aloc_...`)
|
|
49
|
+
* `ALOC_AGENT_FRAMEWORK`: Optional identifier (e.g. `cursor`, `custom-ai-tutor`)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Security & Anti-Scraping
|
|
54
|
+
|
|
55
|
+
Every MCP tool call:
|
|
56
|
+
1. Passes an authenticated API key via `X-API-Key`.
|
|
57
|
+
2. Sends client telemetry headers (`X-Client-Type: mcp`, `X-Is-Agent: true`).
|
|
58
|
+
3. Deducts metered credits matching the ALOC pricing tiers.
|
|
59
|
+
4. Enforces request limits (maximum 20 items per search) to prevent bulk dumping.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface AlocClientConfig {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
agentFramework?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class AlocClient {
|
|
7
|
+
private http;
|
|
8
|
+
private apiKey;
|
|
9
|
+
constructor(config?: AlocClientConfig);
|
|
10
|
+
getQuestions(params: {
|
|
11
|
+
subject: string;
|
|
12
|
+
examType?: string;
|
|
13
|
+
year?: number | string;
|
|
14
|
+
topic?: string;
|
|
15
|
+
difficulty?: string;
|
|
16
|
+
limit?: number;
|
|
17
|
+
}): Promise<any>;
|
|
18
|
+
getQuestionExplanation(questionId: string, depth?: string): Promise<any>;
|
|
19
|
+
getCurriculumTaxonomy(params?: {
|
|
20
|
+
subject?: string;
|
|
21
|
+
examType?: string;
|
|
22
|
+
}): Promise<any>;
|
|
23
|
+
getSimilarQuestions(questionId: string, limit?: number): Promise<any>;
|
|
24
|
+
getSubjects(): Promise<any>;
|
|
25
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AlocClient = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
class AlocClient {
|
|
9
|
+
http;
|
|
10
|
+
apiKey;
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.apiKey = config.apiKey || process.env.ALOC_API_KEY || "";
|
|
13
|
+
const baseUrl = config.baseUrl || process.env.ALOC_BASE_URL || "https://dev.aloc.com.ng/api/v1";
|
|
14
|
+
if (!this.apiKey) {
|
|
15
|
+
console.error("[WARN] ALOC_API_KEY is not set. Authenticated MCP requests will fail until an API key is provided.");
|
|
16
|
+
}
|
|
17
|
+
this.http = axios_1.default.create({
|
|
18
|
+
baseURL: baseUrl,
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
Accept: "application/json",
|
|
22
|
+
"X-API-Key": this.apiKey,
|
|
23
|
+
"X-Client-Type": "mcp",
|
|
24
|
+
"X-Is-Agent": "true",
|
|
25
|
+
"X-Agent-Framework": config.agentFramework || process.env.ALOC_AGENT_FRAMEWORK || "mcp-sdk",
|
|
26
|
+
"User-Agent": "ALOC-MCP-Server/1.0.0 (Node.js)",
|
|
27
|
+
},
|
|
28
|
+
timeout: 15000,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
async getQuestions(params) {
|
|
32
|
+
const response = await this.http.get("/questions", { params });
|
|
33
|
+
return response.data;
|
|
34
|
+
}
|
|
35
|
+
async getQuestionExplanation(questionId, depth) {
|
|
36
|
+
const response = await this.http.post(`/questions/${questionId}/explain`, {
|
|
37
|
+
depth: depth || "step_by_step",
|
|
38
|
+
});
|
|
39
|
+
return response.data;
|
|
40
|
+
}
|
|
41
|
+
async getCurriculumTaxonomy(params) {
|
|
42
|
+
const response = await this.http.get("/curriculum", { params });
|
|
43
|
+
return response.data;
|
|
44
|
+
}
|
|
45
|
+
async getSimilarQuestions(questionId, limit) {
|
|
46
|
+
const response = await this.http.get(`/questions/${questionId}/similar`, {
|
|
47
|
+
params: { limit: limit || 3 },
|
|
48
|
+
});
|
|
49
|
+
return response.data;
|
|
50
|
+
}
|
|
51
|
+
async getSubjects() {
|
|
52
|
+
const response = await this.http.get("/subjects");
|
|
53
|
+
return response.data;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.AlocClient = AlocClient;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
|
+
const client_js_1 = require("./client.js");
|
|
8
|
+
const client = new client_js_1.AlocClient();
|
|
9
|
+
const server = new index_js_1.Server({
|
|
10
|
+
name: "aloc-mcp-server",
|
|
11
|
+
version: "1.0.0",
|
|
12
|
+
}, {
|
|
13
|
+
capabilities: {
|
|
14
|
+
tools: {},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
// Register available tools
|
|
18
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
19
|
+
return {
|
|
20
|
+
tools: [
|
|
21
|
+
{
|
|
22
|
+
name: "aloc_search_questions",
|
|
23
|
+
description: "Search and retrieve curriculum-aligned examination questions (WAEC, JAMB, NECO) with options, metadata, and difficulty levels. Consumes 1-3 ALOC credits.",
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
subject: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Subject name (e.g. 'mathematics', 'english', 'biology', 'physics', 'chemistry', 'economics')",
|
|
30
|
+
},
|
|
31
|
+
examType: {
|
|
32
|
+
type: "string",
|
|
33
|
+
enum: ["waec", "jamb", "neco", "post-utme"],
|
|
34
|
+
description: "Target African examination body",
|
|
35
|
+
},
|
|
36
|
+
year: {
|
|
37
|
+
type: "number",
|
|
38
|
+
description: "Examination year (e.g. 2022)",
|
|
39
|
+
},
|
|
40
|
+
topic: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Topic filter (e.g. 'algebra', 'calculus', 'photosynthesis')",
|
|
43
|
+
},
|
|
44
|
+
difficulty: {
|
|
45
|
+
type: "string",
|
|
46
|
+
enum: ["easy", "medium", "hard"],
|
|
47
|
+
description: "Calibrated difficulty rating",
|
|
48
|
+
},
|
|
49
|
+
limit: {
|
|
50
|
+
type: "number",
|
|
51
|
+
description: "Number of questions to return (max 20, default 5)",
|
|
52
|
+
default: 5,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ["subject"],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "aloc_get_question_explanation",
|
|
60
|
+
description: "Retrieve step-by-step breakdown, remedial hints, and AI-generated conceptual explanations for a specific question. Consumes 10 ALOC credits.",
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
questionId: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "UUID of the question",
|
|
67
|
+
},
|
|
68
|
+
depth: {
|
|
69
|
+
type: "string",
|
|
70
|
+
enum: ["step_by_step", "concise", "remedial"],
|
|
71
|
+
description: "Explanation granularity",
|
|
72
|
+
default: "step_by_step",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
required: ["questionId"],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "aloc_get_curriculum_taxonomy",
|
|
80
|
+
description: "Retrieve official African examination curriculum structure, subject breakdown, and topic hierarchy.",
|
|
81
|
+
inputSchema: {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
subject: {
|
|
85
|
+
type: "string",
|
|
86
|
+
description: "Subject name",
|
|
87
|
+
},
|
|
88
|
+
examType: {
|
|
89
|
+
type: "string",
|
|
90
|
+
enum: ["waec", "jamb", "neco"],
|
|
91
|
+
description: "Examination body",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "aloc_find_similar_questions",
|
|
98
|
+
description: "Find conceptually similar or variant examination questions using semantic vector similarity. Consumes 10 ALOC credits.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
questionId: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "UUID of the source question",
|
|
105
|
+
},
|
|
106
|
+
limit: {
|
|
107
|
+
type: "number",
|
|
108
|
+
description: "Number of similar questions to return (max 10, default 3)",
|
|
109
|
+
default: 3,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ["questionId"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
// Handle tool execution
|
|
119
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
120
|
+
const { name, arguments: args } = request.params;
|
|
121
|
+
try {
|
|
122
|
+
switch (name) {
|
|
123
|
+
case "aloc_search_questions": {
|
|
124
|
+
const { subject, examType, year, topic, difficulty, limit } = (args || {});
|
|
125
|
+
const result = await client.getQuestions({
|
|
126
|
+
subject,
|
|
127
|
+
examType,
|
|
128
|
+
year,
|
|
129
|
+
topic,
|
|
130
|
+
difficulty,
|
|
131
|
+
limit: Math.min(limit || 5, 20),
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "text",
|
|
137
|
+
text: JSON.stringify(result, null, 2),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
case "aloc_get_question_explanation": {
|
|
143
|
+
const { questionId, depth } = (args || {});
|
|
144
|
+
const result = await client.getQuestionExplanation(questionId, depth);
|
|
145
|
+
return {
|
|
146
|
+
content: [
|
|
147
|
+
{
|
|
148
|
+
type: "text",
|
|
149
|
+
text: JSON.stringify(result, null, 2),
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
case "aloc_get_curriculum_taxonomy": {
|
|
155
|
+
const { subject, examType } = (args || {});
|
|
156
|
+
const result = await client.getCurriculumTaxonomy({ subject, examType });
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: "text",
|
|
161
|
+
text: JSON.stringify(result, null, 2),
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
case "aloc_find_similar_questions": {
|
|
167
|
+
const { questionId, limit } = (args || {});
|
|
168
|
+
const result = await client.getSimilarQuestions(questionId, limit);
|
|
169
|
+
return {
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: "text",
|
|
173
|
+
text: JSON.stringify(result, null, 2),
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
default:
|
|
179
|
+
throw new Error(`Unknown ALOC tool: ${name}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
const errorMessage = error.response?.data?.message || error.response?.data?.error || error.message || "Unknown error";
|
|
184
|
+
return {
|
|
185
|
+
isError: true,
|
|
186
|
+
content: [
|
|
187
|
+
{
|
|
188
|
+
type: "text",
|
|
189
|
+
text: `[ALOC Error] ${errorMessage}`,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
// Start transport
|
|
196
|
+
async function main() {
|
|
197
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
198
|
+
await server.connect(transport);
|
|
199
|
+
console.error("ALOC MCP Server running on stdio");
|
|
200
|
+
}
|
|
201
|
+
main().catch((error) => {
|
|
202
|
+
console.error("Fatal error starting ALOC MCP Server:", error);
|
|
203
|
+
process.exit(1);
|
|
204
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@massteck/aloc-mcp-server",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Official Model Context Protocol (MCP) Server for ALOC assessment content infrastructure",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"aloc-mcp-server": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public",
|
|
12
|
+
"registry": "https://registry.npmjs.org"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"start": "node dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"aloc",
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"ai-agent",
|
|
23
|
+
"edtech",
|
|
24
|
+
"curriculum",
|
|
25
|
+
"assessment"
|
|
26
|
+
],
|
|
27
|
+
"author": "ALOC Infrastructure Team",
|
|
28
|
+
"license": "Proprietary",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.6.0",
|
|
31
|
+
"axios": "^1.7.9",
|
|
32
|
+
"zod": "^3.24.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.13.5",
|
|
36
|
+
"typescript": "^5.7.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from "axios";
|
|
2
|
+
|
|
3
|
+
export interface AlocClientConfig {
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
agentFramework?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class AlocClient {
|
|
10
|
+
private http: AxiosInstance;
|
|
11
|
+
private apiKey: string;
|
|
12
|
+
|
|
13
|
+
constructor(config: AlocClientConfig = {}) {
|
|
14
|
+
this.apiKey = config.apiKey || process.env.ALOC_API_KEY || "";
|
|
15
|
+
const baseUrl = config.baseUrl || process.env.ALOC_BASE_URL || "https://dev.aloc.com.ng/api/v1";
|
|
16
|
+
|
|
17
|
+
if (!this.apiKey) {
|
|
18
|
+
console.error(
|
|
19
|
+
"[WARN] ALOC_API_KEY is not set. Authenticated MCP requests will fail until an API key is provided."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.http = axios.create({
|
|
24
|
+
baseURL: baseUrl,
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
"X-API-Key": this.apiKey,
|
|
29
|
+
"X-Client-Type": "mcp",
|
|
30
|
+
"X-Is-Agent": "true",
|
|
31
|
+
"X-Agent-Framework": config.agentFramework || process.env.ALOC_AGENT_FRAMEWORK || "mcp-sdk",
|
|
32
|
+
"User-Agent": "ALOC-MCP-Server/1.0.0 (Node.js)",
|
|
33
|
+
},
|
|
34
|
+
timeout: 15000,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public async getQuestions(params: {
|
|
39
|
+
subject: string;
|
|
40
|
+
examType?: string;
|
|
41
|
+
year?: number | string;
|
|
42
|
+
topic?: string;
|
|
43
|
+
difficulty?: string;
|
|
44
|
+
limit?: number;
|
|
45
|
+
}) {
|
|
46
|
+
const response = await this.http.get("/questions", { params });
|
|
47
|
+
return response.data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async getQuestionExplanation(questionId: string, depth?: string) {
|
|
51
|
+
const response = await this.http.post(`/questions/${questionId}/explain`, {
|
|
52
|
+
depth: depth || "step_by_step",
|
|
53
|
+
});
|
|
54
|
+
return response.data;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async getCurriculumTaxonomy(params?: { subject?: string; examType?: string }) {
|
|
58
|
+
const response = await this.http.get("/curriculum", { params });
|
|
59
|
+
return response.data;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public async getSimilarQuestions(questionId: string, limit?: number) {
|
|
63
|
+
const response = await this.http.get(`/questions/${questionId}/similar`, {
|
|
64
|
+
params: { limit: limit || 3 },
|
|
65
|
+
});
|
|
66
|
+
return response.data;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public async getSubjects() {
|
|
70
|
+
const response = await this.http.get("/subjects");
|
|
71
|
+
return response.data;
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { AlocClient } from "./client.js";
|
|
10
|
+
|
|
11
|
+
const client = new AlocClient();
|
|
12
|
+
|
|
13
|
+
const server = new Server(
|
|
14
|
+
{
|
|
15
|
+
name: "aloc-mcp-server",
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
capabilities: {
|
|
20
|
+
tools: {},
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Register available tools
|
|
26
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
27
|
+
return {
|
|
28
|
+
tools: [
|
|
29
|
+
{
|
|
30
|
+
name: "aloc_search_questions",
|
|
31
|
+
description:
|
|
32
|
+
"Search and retrieve curriculum-aligned examination questions (WAEC, JAMB, NECO) with options, metadata, and difficulty levels. Consumes 1-3 ALOC credits.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
subject: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Subject name (e.g. 'mathematics', 'english', 'biology', 'physics', 'chemistry', 'economics')",
|
|
39
|
+
},
|
|
40
|
+
examType: {
|
|
41
|
+
type: "string",
|
|
42
|
+
enum: ["waec", "jamb", "neco", "post-utme"],
|
|
43
|
+
description: "Target African examination body",
|
|
44
|
+
},
|
|
45
|
+
year: {
|
|
46
|
+
type: "number",
|
|
47
|
+
description: "Examination year (e.g. 2022)",
|
|
48
|
+
},
|
|
49
|
+
topic: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Topic filter (e.g. 'algebra', 'calculus', 'photosynthesis')",
|
|
52
|
+
},
|
|
53
|
+
difficulty: {
|
|
54
|
+
type: "string",
|
|
55
|
+
enum: ["easy", "medium", "hard"],
|
|
56
|
+
description: "Calibrated difficulty rating",
|
|
57
|
+
},
|
|
58
|
+
limit: {
|
|
59
|
+
type: "number",
|
|
60
|
+
description: "Number of questions to return (max 20, default 5)",
|
|
61
|
+
default: 5,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
required: ["subject"],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: "aloc_get_question_explanation",
|
|
69
|
+
description:
|
|
70
|
+
"Retrieve step-by-step breakdown, remedial hints, and AI-generated conceptual explanations for a specific question. Consumes 10 ALOC credits.",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
questionId: {
|
|
75
|
+
type: "string",
|
|
76
|
+
description: "UUID of the question",
|
|
77
|
+
},
|
|
78
|
+
depth: {
|
|
79
|
+
type: "string",
|
|
80
|
+
enum: ["step_by_step", "concise", "remedial"],
|
|
81
|
+
description: "Explanation granularity",
|
|
82
|
+
default: "step_by_step",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
required: ["questionId"],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "aloc_get_curriculum_taxonomy",
|
|
90
|
+
description:
|
|
91
|
+
"Retrieve official African examination curriculum structure, subject breakdown, and topic hierarchy.",
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
subject: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Subject name",
|
|
98
|
+
},
|
|
99
|
+
examType: {
|
|
100
|
+
type: "string",
|
|
101
|
+
enum: ["waec", "jamb", "neco"],
|
|
102
|
+
description: "Examination body",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "aloc_find_similar_questions",
|
|
109
|
+
description:
|
|
110
|
+
"Find conceptually similar or variant examination questions using semantic vector similarity. Consumes 10 ALOC credits.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: {
|
|
114
|
+
questionId: {
|
|
115
|
+
type: "string",
|
|
116
|
+
description: "UUID of the source question",
|
|
117
|
+
},
|
|
118
|
+
limit: {
|
|
119
|
+
type: "number",
|
|
120
|
+
description: "Number of similar questions to return (max 10, default 3)",
|
|
121
|
+
default: 3,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
required: ["questionId"],
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Handle tool execution
|
|
132
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
133
|
+
const { name, arguments: args } = request.params;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
switch (name) {
|
|
137
|
+
case "aloc_search_questions": {
|
|
138
|
+
const { subject, examType, year, topic, difficulty, limit } = (args || {}) as any;
|
|
139
|
+
const result = await client.getQuestions({
|
|
140
|
+
subject,
|
|
141
|
+
examType,
|
|
142
|
+
year,
|
|
143
|
+
topic,
|
|
144
|
+
difficulty,
|
|
145
|
+
limit: Math.min(limit || 5, 20),
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
content: [
|
|
149
|
+
{
|
|
150
|
+
type: "text",
|
|
151
|
+
text: JSON.stringify(result, null, 2),
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
case "aloc_get_question_explanation": {
|
|
158
|
+
const { questionId, depth } = (args || {}) as any;
|
|
159
|
+
const result = await client.getQuestionExplanation(questionId, depth);
|
|
160
|
+
return {
|
|
161
|
+
content: [
|
|
162
|
+
{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: JSON.stringify(result, null, 2),
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case "aloc_get_curriculum_taxonomy": {
|
|
171
|
+
const { subject, examType } = (args || {}) as any;
|
|
172
|
+
const result = await client.getCurriculumTaxonomy({ subject, examType });
|
|
173
|
+
return {
|
|
174
|
+
content: [
|
|
175
|
+
{
|
|
176
|
+
type: "text",
|
|
177
|
+
text: JSON.stringify(result, null, 2),
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
case "aloc_find_similar_questions": {
|
|
184
|
+
const { questionId, limit } = (args || {}) as any;
|
|
185
|
+
const result = await client.getSimilarQuestions(questionId, limit);
|
|
186
|
+
return {
|
|
187
|
+
content: [
|
|
188
|
+
{
|
|
189
|
+
type: "text",
|
|
190
|
+
text: JSON.stringify(result, null, 2),
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
default:
|
|
197
|
+
throw new Error(`Unknown ALOC tool: ${name}`);
|
|
198
|
+
}
|
|
199
|
+
} catch (error: any) {
|
|
200
|
+
const errorMessage =
|
|
201
|
+
error.response?.data?.message || error.response?.data?.error || error.message || "Unknown error";
|
|
202
|
+
return {
|
|
203
|
+
isError: true,
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: "text",
|
|
207
|
+
text: `[ALOC Error] ${errorMessage}`,
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Start transport
|
|
215
|
+
async function main() {
|
|
216
|
+
const transport = new StdioServerTransport();
|
|
217
|
+
await server.connect(transport);
|
|
218
|
+
console.error("ALOC MCP Server running on stdio");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
main().catch((error) => {
|
|
222
|
+
console.error("Fatal error starting ALOC MCP Server:", error);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"]
|
|
16
|
+
}
|