loaditout-mcp-server 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/README.md +57 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +742 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @loaditout/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for discovering, installing, and managing AI agent skills from the Loaditout registry. It connects your agent to a curated catalog of MCP servers and SKILL.md behaviors, with search, recommendations, and usage reporting.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Claude Code
|
|
8
|
+
|
|
9
|
+
Add to your Claude Code MCP settings (`~/.claude/settings.json`):
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"loaditout": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["-y", "@loaditout/mcp-server"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Cursor
|
|
23
|
+
|
|
24
|
+
Add to your Cursor MCP config (`.cursor/mcp.json` in your project or `~/.cursor/mcp.json` globally):
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"mcpServers": {
|
|
29
|
+
"loaditout": {
|
|
30
|
+
"command": "npx",
|
|
31
|
+
"args": ["-y", "@loaditout/mcp-server"]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Available Tools
|
|
38
|
+
|
|
39
|
+
| Tool | Description |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `search_skills` | Search the registry for skills by natural language query. Supports filtering by type and agent platform. |
|
|
42
|
+
| `get_skill` | Get full details for a specific skill by its slug (owner/repo format). |
|
|
43
|
+
| `install_skill` | Get the exact configuration JSON needed to install a skill for a specific agent. Returns config and instructions but does not write files. |
|
|
44
|
+
| `list_categories` | List all skill categories with descriptions and skill counts. |
|
|
45
|
+
| `recommend_skills` | Get skill recommendations based on a description of your project or task. |
|
|
46
|
+
| `check_capability_gap` | Analyze what the agent is trying to do and suggest skills that would help. Designed for when you encounter a task you cannot complete or need specialized tools for. |
|
|
47
|
+
| `report_skill_usage` | Report whether an installed skill worked correctly. Helps improve quality scores across the registry. |
|
|
48
|
+
|
|
49
|
+
## Resources
|
|
50
|
+
|
|
51
|
+
| URI | Description |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `loaditout://catalog` | Browse the full skill catalog organized by category with counts. |
|
|
54
|
+
|
|
55
|
+
## Zero Dependencies
|
|
56
|
+
|
|
57
|
+
This server uses only Node.js built-in modules (https, readline) for HTTP communication. No external packages are required.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const https = __importStar(require("https"));
|
|
38
|
+
const readline = __importStar(require("readline"));
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const os = __importStar(require("os"));
|
|
43
|
+
const API_BASE = "https://loaditout.ai/api/agent";
|
|
44
|
+
const SERVER_NAME = "loaditout";
|
|
45
|
+
const SERVER_VERSION = "0.1.0";
|
|
46
|
+
// --- Agent key management ---
|
|
47
|
+
const AGENT_KEY_PATH = path.join(os.homedir(), ".loaditout", "agent-key");
|
|
48
|
+
function getOrCreateAgentKey() {
|
|
49
|
+
try {
|
|
50
|
+
if (fs.existsSync(AGENT_KEY_PATH)) {
|
|
51
|
+
const key = fs.readFileSync(AGENT_KEY_PATH, "utf-8").trim();
|
|
52
|
+
if (key.length > 0 && key.length <= 64)
|
|
53
|
+
return key;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Fall through to create
|
|
58
|
+
}
|
|
59
|
+
const key = crypto.randomUUID().replace(/-/g, "").slice(0, 32);
|
|
60
|
+
try {
|
|
61
|
+
fs.mkdirSync(path.dirname(AGENT_KEY_PATH), { recursive: true });
|
|
62
|
+
fs.writeFileSync(AGENT_KEY_PATH, key, "utf-8");
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// If we cannot persist, use an ephemeral key
|
|
66
|
+
}
|
|
67
|
+
return key;
|
|
68
|
+
}
|
|
69
|
+
const AGENT_KEY = getOrCreateAgentKey();
|
|
70
|
+
// --- HTTP helper ---
|
|
71
|
+
function fetchJSON(url, extraHeaders) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
const headers = {
|
|
74
|
+
"User-Agent": `loaditout-mcp/${SERVER_VERSION}`,
|
|
75
|
+
...extraHeaders,
|
|
76
|
+
};
|
|
77
|
+
https
|
|
78
|
+
.get(url, { headers }, (res) => {
|
|
79
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
80
|
+
const location = res.headers.location;
|
|
81
|
+
if (location) {
|
|
82
|
+
fetchJSON(location).then(resolve, reject);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (res.statusCode !== 200) {
|
|
87
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
let data = "";
|
|
91
|
+
res.on("data", (chunk) => (data += chunk));
|
|
92
|
+
res.on("end", () => {
|
|
93
|
+
try {
|
|
94
|
+
resolve(JSON.parse(data));
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
reject(new Error("Invalid JSON response"));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
})
|
|
101
|
+
.on("error", reject);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function postJSON(url, body) {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const payload = JSON.stringify(body);
|
|
107
|
+
const parsed = new URL(url);
|
|
108
|
+
const options = {
|
|
109
|
+
hostname: parsed.hostname,
|
|
110
|
+
port: parsed.port || 443,
|
|
111
|
+
path: parsed.pathname + parsed.search,
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
116
|
+
"User-Agent": `loaditout-mcp/${SERVER_VERSION}`,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const req = https.request(options, (res) => {
|
|
120
|
+
let data = "";
|
|
121
|
+
res.on("data", (chunk) => (data += chunk));
|
|
122
|
+
res.on("end", () => {
|
|
123
|
+
try {
|
|
124
|
+
resolve(JSON.parse(data));
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
resolve({ status: res.statusCode });
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
req.on("error", reject);
|
|
132
|
+
req.write(payload);
|
|
133
|
+
req.end();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function deleteJSON(url) {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const parsed = new URL(url);
|
|
139
|
+
const options = {
|
|
140
|
+
hostname: parsed.hostname,
|
|
141
|
+
port: parsed.port || 443,
|
|
142
|
+
path: parsed.pathname + parsed.search,
|
|
143
|
+
method: "DELETE",
|
|
144
|
+
headers: {
|
|
145
|
+
"User-Agent": `loaditout-mcp/${SERVER_VERSION}`,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
const req = https.request(options, (res) => {
|
|
149
|
+
let data = "";
|
|
150
|
+
res.on("data", (chunk) => (data += chunk));
|
|
151
|
+
res.on("end", () => {
|
|
152
|
+
try {
|
|
153
|
+
resolve(JSON.parse(data));
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
resolve({ status: res.statusCode });
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
req.on("error", reject);
|
|
161
|
+
req.end();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// --- Tool definitions ---
|
|
165
|
+
const TOOLS = [
|
|
166
|
+
{
|
|
167
|
+
name: "search_skills",
|
|
168
|
+
description: "Search the Loaditout registry for AI agent skills (MCP servers, SKILL.md behaviors). Returns matching skills with descriptions, types, and quality scores.",
|
|
169
|
+
inputSchema: {
|
|
170
|
+
type: "object",
|
|
171
|
+
properties: {
|
|
172
|
+
query: {
|
|
173
|
+
type: "string",
|
|
174
|
+
description: "Natural language search query. Examples: 'postgres database', 'browser automation', 'github issues'",
|
|
175
|
+
},
|
|
176
|
+
type: {
|
|
177
|
+
type: "string",
|
|
178
|
+
enum: ["mcp-tool", "skill-md", "hybrid"],
|
|
179
|
+
description: "Filter by skill type",
|
|
180
|
+
},
|
|
181
|
+
agent: {
|
|
182
|
+
type: "string",
|
|
183
|
+
enum: ["claude-code", "cursor", "codex-cli", "windsurf"],
|
|
184
|
+
description: "Filter by compatible agent platform",
|
|
185
|
+
},
|
|
186
|
+
limit: {
|
|
187
|
+
type: "number",
|
|
188
|
+
description: "Max results to return (default 10, max 25)",
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
required: ["query"],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "get_skill",
|
|
196
|
+
description: "Get full details for a specific skill including description, install config, compatibility, and quality score.",
|
|
197
|
+
inputSchema: {
|
|
198
|
+
type: "object",
|
|
199
|
+
properties: {
|
|
200
|
+
slug: {
|
|
201
|
+
type: "string",
|
|
202
|
+
description: "Skill slug in owner/repo format. Example: 'modelcontextprotocol/server-postgres'",
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
required: ["slug"],
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
name: "install_skill",
|
|
210
|
+
description: "Get the exact configuration JSON needed to install a skill for a specific agent. Returns the config and instructions but does NOT write any files.",
|
|
211
|
+
inputSchema: {
|
|
212
|
+
type: "object",
|
|
213
|
+
properties: {
|
|
214
|
+
slug: {
|
|
215
|
+
type: "string",
|
|
216
|
+
description: "Skill slug in owner/repo format",
|
|
217
|
+
},
|
|
218
|
+
agent: {
|
|
219
|
+
type: "string",
|
|
220
|
+
enum: ["claude-code", "cursor", "codex-cli", "windsurf", "generic"],
|
|
221
|
+
description: "Target agent platform",
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
required: ["slug", "agent"],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "list_categories",
|
|
229
|
+
description: "List all skill categories in the Loaditout registry with descriptions and skill counts.",
|
|
230
|
+
inputSchema: {
|
|
231
|
+
type: "object",
|
|
232
|
+
properties: {},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "recommend_skills",
|
|
237
|
+
description: "Get skill recommendations based on what you are working on. Describe your project or task and get relevant skill suggestions.",
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: "object",
|
|
240
|
+
properties: {
|
|
241
|
+
context: {
|
|
242
|
+
type: "string",
|
|
243
|
+
description: "Description of what you are working on. Example: 'building a Next.js app with Supabase and Stripe'",
|
|
244
|
+
},
|
|
245
|
+
installed: {
|
|
246
|
+
type: "string",
|
|
247
|
+
description: "Comma-separated slugs of already-installed skills to avoid duplicates",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
required: ["context"],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "check_capability_gap",
|
|
255
|
+
description: "Analyze what the agent is trying to do and suggest skills that would help. Call this when you encounter a task you can't complete or need specialized tools for.",
|
|
256
|
+
inputSchema: {
|
|
257
|
+
type: "object",
|
|
258
|
+
properties: {
|
|
259
|
+
task: {
|
|
260
|
+
type: "string",
|
|
261
|
+
description: "What the agent is trying to do",
|
|
262
|
+
},
|
|
263
|
+
current_tools: {
|
|
264
|
+
type: "array",
|
|
265
|
+
items: { type: "string" },
|
|
266
|
+
description: "Tools the agent already has",
|
|
267
|
+
},
|
|
268
|
+
agent_type: {
|
|
269
|
+
type: "string",
|
|
270
|
+
enum: ["claude-code", "cursor", "codex-cli", "windsurf"],
|
|
271
|
+
description: "The agent platform being used",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
required: ["task", "current_tools", "agent_type"],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: "report_skill_usage",
|
|
279
|
+
description: "Report whether an installed skill worked correctly. Helps improve quality scores.",
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: "object",
|
|
282
|
+
properties: {
|
|
283
|
+
slug: {
|
|
284
|
+
type: "string",
|
|
285
|
+
description: "Skill slug in owner/repo format",
|
|
286
|
+
},
|
|
287
|
+
status: {
|
|
288
|
+
type: "string",
|
|
289
|
+
enum: ["success", "error", "partial"],
|
|
290
|
+
description: "Whether the skill worked correctly",
|
|
291
|
+
},
|
|
292
|
+
error_message: {
|
|
293
|
+
type: "string",
|
|
294
|
+
description: "Error details if the skill failed or partially worked",
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
required: ["slug", "status"],
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: "save_memory",
|
|
302
|
+
description: "Save a key-value pair to persistent agent memory. Survives across sessions. Use this to remember installed skills, preferences, search context, or any data you want to recall later.",
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: "object",
|
|
305
|
+
properties: {
|
|
306
|
+
key: {
|
|
307
|
+
type: "string",
|
|
308
|
+
description: "Memory key name. Examples: 'installed_skills', 'preferred_categories', 'project_context'",
|
|
309
|
+
},
|
|
310
|
+
value: {
|
|
311
|
+
description: "Any JSON-serializable value to store (string, array, object, number, etc.)",
|
|
312
|
+
},
|
|
313
|
+
type: {
|
|
314
|
+
type: "string",
|
|
315
|
+
enum: ["search", "install", "preference", "context"],
|
|
316
|
+
description: "Category of this memory entry",
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
required: ["key", "value", "type"],
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "recall_memory",
|
|
324
|
+
description: "Retrieve previously saved memories. Returns all stored key-value pairs, optionally filtered by type. Use this at the start of a session to restore context.",
|
|
325
|
+
inputSchema: {
|
|
326
|
+
type: "object",
|
|
327
|
+
properties: {
|
|
328
|
+
type: {
|
|
329
|
+
type: "string",
|
|
330
|
+
enum: ["search", "install", "preference", "context"],
|
|
331
|
+
description: "Filter memories by type. Omit to get all memories.",
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: "request_permission",
|
|
338
|
+
description: "Request permission from the human owner to install a skill. Use this when a skill requires explicit approval before installation. Returns a request ID to check status later.",
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: "object",
|
|
341
|
+
properties: {
|
|
342
|
+
slug: {
|
|
343
|
+
type: "string",
|
|
344
|
+
description: "Skill slug in owner/repo format",
|
|
345
|
+
},
|
|
346
|
+
reason: {
|
|
347
|
+
type: "string",
|
|
348
|
+
description: "Why the agent wants this skill. Example: 'I need database access to complete the migration task'",
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
required: ["slug", "reason"],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: "check_permission",
|
|
356
|
+
description: "Check the status of a previously submitted permission request. Returns 'pending', 'approved', or 'denied'.",
|
|
357
|
+
inputSchema: {
|
|
358
|
+
type: "object",
|
|
359
|
+
properties: {
|
|
360
|
+
request_id: {
|
|
361
|
+
type: "number",
|
|
362
|
+
description: "The request ID returned from request_permission",
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
required: ["request_id"],
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
name: "install_batch",
|
|
370
|
+
description: "Install multiple skills at once. Returns install configs for all requested skills in a single call.",
|
|
371
|
+
inputSchema: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
slugs: {
|
|
375
|
+
type: "array",
|
|
376
|
+
items: { type: "string" },
|
|
377
|
+
description: "Array of skill slugs in owner/repo format. Maximum 20.",
|
|
378
|
+
},
|
|
379
|
+
agent: {
|
|
380
|
+
type: "string",
|
|
381
|
+
enum: ["claude-code", "cursor", "codex-cli", "windsurf", "generic"],
|
|
382
|
+
description: "Target agent platform",
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
required: ["slugs", "agent"],
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "share_loadout",
|
|
390
|
+
description: "Get this agent's public loadout (installed skills, trust score, stats). Use this to share your current skill configuration.",
|
|
391
|
+
inputSchema: {
|
|
392
|
+
type: "object",
|
|
393
|
+
properties: {},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: "install_pack",
|
|
398
|
+
description: "Install an entire Agent Pack (curated skill bundle). Returns install configs for all skills in the pack. Use this to quickly set up your agent for a specific role like research, full-stack development, DevOps, etc.",
|
|
399
|
+
inputSchema: {
|
|
400
|
+
type: "object",
|
|
401
|
+
properties: {
|
|
402
|
+
slug: {
|
|
403
|
+
type: "string",
|
|
404
|
+
description: "Pack slug. Examples: 'research-agent', 'full-stack-developer', 'devops-engineer', 'data-engineer', 'browser-automation'",
|
|
405
|
+
},
|
|
406
|
+
agent: {
|
|
407
|
+
type: "string",
|
|
408
|
+
enum: ["claude-code", "cursor", "codex-cli", "windsurf", "generic"],
|
|
409
|
+
description: "Target agent platform",
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
required: ["slug", "agent"],
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
];
|
|
416
|
+
// --- Tool handlers ---
|
|
417
|
+
async function handleSearchSkills(args) {
|
|
418
|
+
const params = new URLSearchParams({ q: args.query });
|
|
419
|
+
if (args.type)
|
|
420
|
+
params.set("type", args.type);
|
|
421
|
+
if (args.agent)
|
|
422
|
+
params.set("agent", args.agent);
|
|
423
|
+
if (args.limit)
|
|
424
|
+
params.set("limit", String(args.limit));
|
|
425
|
+
const result = await fetchJSON(`${API_BASE}/search?${params.toString()}`);
|
|
426
|
+
return JSON.stringify(result, null, 2);
|
|
427
|
+
}
|
|
428
|
+
async function handleGetSkill(args) {
|
|
429
|
+
const result = await fetchJSON(`${API_BASE}/skill/${encodeURIComponent(args.slug)}`);
|
|
430
|
+
return JSON.stringify(result, null, 2);
|
|
431
|
+
}
|
|
432
|
+
async function handleInstallSkill(args) {
|
|
433
|
+
const params = new URLSearchParams({ agent: args.agent });
|
|
434
|
+
const result = await fetchJSON(`${API_BASE}/install/${encodeURIComponent(args.slug)}?${params.toString()}`, { "X-Agent-Key": AGENT_KEY });
|
|
435
|
+
return JSON.stringify(result, null, 2);
|
|
436
|
+
}
|
|
437
|
+
async function handleListCategories() {
|
|
438
|
+
const result = await fetchJSON(`${API_BASE}/categories`);
|
|
439
|
+
return JSON.stringify(result, null, 2);
|
|
440
|
+
}
|
|
441
|
+
async function handleRecommendSkills(args) {
|
|
442
|
+
const params = new URLSearchParams({ context: args.context });
|
|
443
|
+
if (args.installed)
|
|
444
|
+
params.set("installed", args.installed);
|
|
445
|
+
const result = await fetchJSON(`${API_BASE}/recommend?${params.toString()}`, { "X-Agent-Key": AGENT_KEY });
|
|
446
|
+
return JSON.stringify(result, null, 2);
|
|
447
|
+
}
|
|
448
|
+
async function handleCheckCapabilityGap(args) {
|
|
449
|
+
const params = new URLSearchParams({
|
|
450
|
+
context: args.task,
|
|
451
|
+
installed: args.current_tools.join(","),
|
|
452
|
+
});
|
|
453
|
+
const result = (await fetchJSON(`${API_BASE}/recommend?${params.toString()}`));
|
|
454
|
+
const recommendations = result.results ?? [];
|
|
455
|
+
if (recommendations.length === 0) {
|
|
456
|
+
return "No additional skills needed for this task.";
|
|
457
|
+
}
|
|
458
|
+
const lines = ["Based on what you're doing, these skills might help:\n"];
|
|
459
|
+
recommendations.forEach((r, i) => {
|
|
460
|
+
const name = r.name ?? r.slug ?? "unknown";
|
|
461
|
+
const score = r.quality_score ?? 0;
|
|
462
|
+
const desc = r.description ?? "";
|
|
463
|
+
lines.push(`${i + 1}. ${r.slug} (quality: ${score}) - ${desc}`);
|
|
464
|
+
lines.push(` Install: npx loaditout add ${r.slug}`);
|
|
465
|
+
});
|
|
466
|
+
return lines.join("\n");
|
|
467
|
+
}
|
|
468
|
+
async function handleReportSkillUsage(args) {
|
|
469
|
+
const body = {
|
|
470
|
+
slug: args.slug,
|
|
471
|
+
status: args.status,
|
|
472
|
+
};
|
|
473
|
+
if (args.error_message) {
|
|
474
|
+
body.error_message = args.error_message;
|
|
475
|
+
}
|
|
476
|
+
const result = await postJSON(`${API_BASE}/report`, body);
|
|
477
|
+
return JSON.stringify(result, null, 2);
|
|
478
|
+
}
|
|
479
|
+
async function handleSaveMemory(args) {
|
|
480
|
+
const body = {
|
|
481
|
+
agent_key: AGENT_KEY,
|
|
482
|
+
key: args.key,
|
|
483
|
+
value: args.value,
|
|
484
|
+
type: args.type,
|
|
485
|
+
};
|
|
486
|
+
const result = await postJSON(`${API_BASE}/memory`, body);
|
|
487
|
+
return JSON.stringify(result, null, 2);
|
|
488
|
+
}
|
|
489
|
+
async function handleRecallMemory(args) {
|
|
490
|
+
const params = new URLSearchParams({ agent_key: AGENT_KEY });
|
|
491
|
+
if (args.type)
|
|
492
|
+
params.set("type", args.type);
|
|
493
|
+
const result = await fetchJSON(`${API_BASE}/memory?${params.toString()}`);
|
|
494
|
+
return JSON.stringify(result, null, 2);
|
|
495
|
+
}
|
|
496
|
+
async function handleRequestPermission(args) {
|
|
497
|
+
const body = {
|
|
498
|
+
agent_key: AGENT_KEY,
|
|
499
|
+
slug: args.slug,
|
|
500
|
+
reason: args.reason,
|
|
501
|
+
};
|
|
502
|
+
const result = (await postJSON(`${API_BASE}/permission`, body));
|
|
503
|
+
if (result.error) {
|
|
504
|
+
return `Permission request failed: ${result.error}`;
|
|
505
|
+
}
|
|
506
|
+
return `Permission requested (ID: ${result.request_id}). Status: ${result.status}. Waiting for human approval.`;
|
|
507
|
+
}
|
|
508
|
+
async function handleCheckPermission(args) {
|
|
509
|
+
const params = new URLSearchParams({
|
|
510
|
+
agent_key: AGENT_KEY,
|
|
511
|
+
request_id: String(args.request_id),
|
|
512
|
+
});
|
|
513
|
+
const result = (await fetchJSON(`${API_BASE}/permission?${params.toString()}`));
|
|
514
|
+
if (result.error) {
|
|
515
|
+
return `Failed to check permission: ${result.error}`;
|
|
516
|
+
}
|
|
517
|
+
return `Permission status: ${result.status}`;
|
|
518
|
+
}
|
|
519
|
+
async function handleInstallBatch(args) {
|
|
520
|
+
const body = {
|
|
521
|
+
slugs: args.slugs,
|
|
522
|
+
agent: args.agent,
|
|
523
|
+
};
|
|
524
|
+
const result = await postJSON(`${API_BASE}/install-batch`, body);
|
|
525
|
+
return JSON.stringify(result, null, 2);
|
|
526
|
+
}
|
|
527
|
+
async function handleShareLoadout() {
|
|
528
|
+
const result = await fetchJSON(`${API_BASE}/loadout/${encodeURIComponent(AGENT_KEY)}`);
|
|
529
|
+
return JSON.stringify(result, null, 2);
|
|
530
|
+
}
|
|
531
|
+
async function handleInstallPack(args) {
|
|
532
|
+
const params = new URLSearchParams({ agent: args.agent });
|
|
533
|
+
const result = (await fetchJSON(`${API_BASE}/pack/${encodeURIComponent(args.slug)}?${params.toString()}`));
|
|
534
|
+
if (result.error) {
|
|
535
|
+
return `Pack not found: ${result.error}`;
|
|
536
|
+
}
|
|
537
|
+
const packSkills = result.skills ?? [];
|
|
538
|
+
const lines = [
|
|
539
|
+
`Agent Pack: ${result.name ?? result.slug}`,
|
|
540
|
+
`Outcome: ${result.target_outcome ?? "N/A"}`,
|
|
541
|
+
`Skills: ${packSkills.length}`,
|
|
542
|
+
`Install all: ${result.install_all_command ?? `npx loaditout add-pack ${args.slug}`}`,
|
|
543
|
+
"",
|
|
544
|
+
"Skills in this pack:",
|
|
545
|
+
"",
|
|
546
|
+
];
|
|
547
|
+
packSkills.forEach((s, i) => {
|
|
548
|
+
lines.push(`${i + 1}. ${s.name ?? s.slug} (${s.type ?? "unknown"})`);
|
|
549
|
+
lines.push(` Slug: ${s.slug}`);
|
|
550
|
+
if (s.install_config) {
|
|
551
|
+
lines.push(` Config: ${JSON.stringify(s.install_config)}`);
|
|
552
|
+
}
|
|
553
|
+
lines.push("");
|
|
554
|
+
});
|
|
555
|
+
return lines.join("\n");
|
|
556
|
+
}
|
|
557
|
+
function makeResponse(id, result) {
|
|
558
|
+
return { jsonrpc: "2.0", id, result };
|
|
559
|
+
}
|
|
560
|
+
function makeError(id, code, message) {
|
|
561
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
562
|
+
}
|
|
563
|
+
function send(response) {
|
|
564
|
+
const json = JSON.stringify(response);
|
|
565
|
+
process.stdout.write(json + "\n");
|
|
566
|
+
}
|
|
567
|
+
async function handleRequest(request) {
|
|
568
|
+
const { id, method, params } = request;
|
|
569
|
+
switch (method) {
|
|
570
|
+
case "initialize": {
|
|
571
|
+
send(makeResponse(id, {
|
|
572
|
+
protocolVersion: "2024-11-05",
|
|
573
|
+
capabilities: {
|
|
574
|
+
tools: {},
|
|
575
|
+
resources: {},
|
|
576
|
+
},
|
|
577
|
+
serverInfo: {
|
|
578
|
+
name: SERVER_NAME,
|
|
579
|
+
version: SERVER_VERSION,
|
|
580
|
+
},
|
|
581
|
+
}));
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
case "notifications/initialized": {
|
|
585
|
+
// No response needed for notifications
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
case "tools/list": {
|
|
589
|
+
send(makeResponse(id, { tools: TOOLS }));
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
case "tools/call": {
|
|
593
|
+
const toolName = params?.name;
|
|
594
|
+
const toolArgs = (params
|
|
595
|
+
?.arguments || {});
|
|
596
|
+
try {
|
|
597
|
+
let resultText;
|
|
598
|
+
switch (toolName) {
|
|
599
|
+
case "search_skills":
|
|
600
|
+
resultText = await handleSearchSkills(toolArgs);
|
|
601
|
+
break;
|
|
602
|
+
case "get_skill":
|
|
603
|
+
resultText = await handleGetSkill(toolArgs);
|
|
604
|
+
break;
|
|
605
|
+
case "install_skill":
|
|
606
|
+
resultText = await handleInstallSkill(toolArgs);
|
|
607
|
+
break;
|
|
608
|
+
case "list_categories":
|
|
609
|
+
resultText = await handleListCategories();
|
|
610
|
+
break;
|
|
611
|
+
case "recommend_skills":
|
|
612
|
+
resultText = await handleRecommendSkills(toolArgs);
|
|
613
|
+
break;
|
|
614
|
+
case "check_capability_gap":
|
|
615
|
+
resultText = await handleCheckCapabilityGap(toolArgs);
|
|
616
|
+
break;
|
|
617
|
+
case "report_skill_usage":
|
|
618
|
+
resultText = await handleReportSkillUsage(toolArgs);
|
|
619
|
+
break;
|
|
620
|
+
case "save_memory":
|
|
621
|
+
resultText = await handleSaveMemory(toolArgs);
|
|
622
|
+
break;
|
|
623
|
+
case "recall_memory":
|
|
624
|
+
resultText = await handleRecallMemory(toolArgs);
|
|
625
|
+
break;
|
|
626
|
+
case "request_permission":
|
|
627
|
+
resultText = await handleRequestPermission(toolArgs);
|
|
628
|
+
break;
|
|
629
|
+
case "check_permission":
|
|
630
|
+
resultText = await handleCheckPermission(toolArgs);
|
|
631
|
+
break;
|
|
632
|
+
case "install_batch":
|
|
633
|
+
resultText = await handleInstallBatch(toolArgs);
|
|
634
|
+
break;
|
|
635
|
+
case "share_loadout":
|
|
636
|
+
resultText = await handleShareLoadout();
|
|
637
|
+
break;
|
|
638
|
+
case "install_pack":
|
|
639
|
+
resultText = await handleInstallPack(toolArgs);
|
|
640
|
+
break;
|
|
641
|
+
default:
|
|
642
|
+
send(makeError(id, -32601, `Unknown tool: ${toolName}`));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
send(makeResponse(id, {
|
|
646
|
+
content: [{ type: "text", text: resultText }],
|
|
647
|
+
}));
|
|
648
|
+
}
|
|
649
|
+
catch (err) {
|
|
650
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
651
|
+
send(makeResponse(id, {
|
|
652
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
653
|
+
isError: true,
|
|
654
|
+
}));
|
|
655
|
+
}
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
case "resources/list": {
|
|
659
|
+
send(makeResponse(id, {
|
|
660
|
+
resources: [
|
|
661
|
+
{
|
|
662
|
+
uri: "loaditout://catalog",
|
|
663
|
+
name: "Loaditout Skill Catalog",
|
|
664
|
+
description: "Browse the full Loaditout skill catalog by category",
|
|
665
|
+
mimeType: "text/plain",
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
}));
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
case "resources/read": {
|
|
672
|
+
const uri = params?.uri;
|
|
673
|
+
if (uri === "loaditout://catalog") {
|
|
674
|
+
try {
|
|
675
|
+
const categories = (await fetchJSON(`${API_BASE}/categories`));
|
|
676
|
+
const cats = categories.categories ?? [];
|
|
677
|
+
const lines = ["Loaditout Skill Catalog\n"];
|
|
678
|
+
for (const cat of cats) {
|
|
679
|
+
const name = cat.name ?? cat.slug ?? "unknown";
|
|
680
|
+
const count = cat.count ?? 0;
|
|
681
|
+
const desc = cat.description ?? "";
|
|
682
|
+
lines.push(`- ${name} (${count} skills): ${desc}`);
|
|
683
|
+
}
|
|
684
|
+
send(makeResponse(id, {
|
|
685
|
+
contents: [
|
|
686
|
+
{
|
|
687
|
+
uri: "loaditout://catalog",
|
|
688
|
+
mimeType: "text/plain",
|
|
689
|
+
text: lines.join("\n"),
|
|
690
|
+
},
|
|
691
|
+
],
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
catch (err) {
|
|
695
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
696
|
+
send(makeError(id, -32603, `Failed to load catalog: ${message}`));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
send(makeError(id, -32602, `Unknown resource: ${uri}`));
|
|
701
|
+
}
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
case "ping": {
|
|
705
|
+
send(makeResponse(id, {}));
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
default: {
|
|
709
|
+
if (id !== undefined && id !== null) {
|
|
710
|
+
send(makeError(id, -32601, `Method not found: ${method}`));
|
|
711
|
+
}
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// --- Main: read JSON-RPC messages from stdin ---
|
|
717
|
+
function main() {
|
|
718
|
+
const rl = readline.createInterface({
|
|
719
|
+
input: process.stdin,
|
|
720
|
+
terminal: false,
|
|
721
|
+
});
|
|
722
|
+
rl.on("line", (line) => {
|
|
723
|
+
const trimmed = line.trim();
|
|
724
|
+
if (!trimmed)
|
|
725
|
+
return;
|
|
726
|
+
let request;
|
|
727
|
+
try {
|
|
728
|
+
request = JSON.parse(trimmed);
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
send(makeError(null, -32700, "Parse error"));
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
handleRequest(request).catch((err) => {
|
|
735
|
+
send(makeError(request.id ?? null, -32603, err instanceof Error ? err.message : String(err)));
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
rl.on("close", () => {
|
|
739
|
+
process.exit(0);
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "loaditout-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for discovering and installing AI agent skills from Loaditout",
|
|
5
|
+
"bin": {
|
|
6
|
+
"loaditout-mcp": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsc --watch",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": ["mcp", "mcp-server", "ai", "agent", "skills", "loaditout"],
|
|
15
|
+
"author": "Anand Jain",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/loaditoutadmin/loaditout"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://loaditout.ai",
|
|
22
|
+
"files": ["dist"],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
}
|
|
26
|
+
}
|