loaditout-mcp-server 0.2.0 → 0.2.2

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.
@@ -0,0 +1,1156 @@
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, extraHeaders) {
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
+ ...extraHeaders,
118
+ },
119
+ };
120
+ const req = https.request(options, (res) => {
121
+ let data = "";
122
+ res.on("data", (chunk) => (data += chunk));
123
+ res.on("end", () => {
124
+ try {
125
+ resolve(JSON.parse(data));
126
+ }
127
+ catch {
128
+ resolve({ status: res.statusCode });
129
+ }
130
+ });
131
+ });
132
+ req.on("error", reject);
133
+ req.write(payload);
134
+ req.end();
135
+ });
136
+ }
137
+ function deleteJSON(url) {
138
+ return new Promise((resolve, reject) => {
139
+ const parsed = new URL(url);
140
+ const options = {
141
+ hostname: parsed.hostname,
142
+ port: parsed.port || 443,
143
+ path: parsed.pathname + parsed.search,
144
+ method: "DELETE",
145
+ headers: {
146
+ "User-Agent": `loaditout-mcp/${SERVER_VERSION}`,
147
+ },
148
+ };
149
+ const req = https.request(options, (res) => {
150
+ let data = "";
151
+ res.on("data", (chunk) => (data += chunk));
152
+ res.on("end", () => {
153
+ try {
154
+ resolve(JSON.parse(data));
155
+ }
156
+ catch {
157
+ resolve({ status: res.statusCode });
158
+ }
159
+ });
160
+ });
161
+ req.on("error", reject);
162
+ req.end();
163
+ });
164
+ }
165
+ // --- Tool definitions ---
166
+ const TOOLS = [
167
+ {
168
+ name: "search_skills",
169
+ description: "Search the Loaditout registry for AI agent skills (MCP servers, SKILL.md behaviors). Returns matching skills with descriptions, types, and quality scores.",
170
+ inputSchema: {
171
+ type: "object",
172
+ properties: {
173
+ query: {
174
+ type: "string",
175
+ description: "Natural language search query. Examples: 'postgres database', 'browser automation', 'github issues'",
176
+ },
177
+ type: {
178
+ type: "string",
179
+ enum: ["mcp-tool", "skill-md", "hybrid"],
180
+ description: "Filter by skill type",
181
+ },
182
+ agent: {
183
+ type: "string",
184
+ enum: ["claude-code", "cursor", "codex-cli", "windsurf"],
185
+ description: "Filter by compatible agent platform",
186
+ },
187
+ limit: {
188
+ type: "number",
189
+ description: "Max results to return (default 10, max 25)",
190
+ },
191
+ },
192
+ required: ["query"],
193
+ },
194
+ },
195
+ {
196
+ name: "get_skill",
197
+ description: "Get full details for a specific skill including description, install config, compatibility, and quality score.",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ slug: {
202
+ type: "string",
203
+ description: "Skill slug in owner/repo format. Example: 'modelcontextprotocol/server-postgres'",
204
+ },
205
+ },
206
+ required: ["slug"],
207
+ },
208
+ },
209
+ {
210
+ name: "install_skill",
211
+ 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.",
212
+ inputSchema: {
213
+ type: "object",
214
+ properties: {
215
+ slug: {
216
+ type: "string",
217
+ description: "Skill slug in owner/repo format",
218
+ },
219
+ agent: {
220
+ type: "string",
221
+ enum: ["claude-code", "cursor", "codex-cli", "windsurf", "generic"],
222
+ description: "Target agent platform",
223
+ },
224
+ },
225
+ required: ["slug", "agent"],
226
+ },
227
+ },
228
+ {
229
+ name: "list_categories",
230
+ description: "List all skill categories in the Loaditout registry with descriptions and skill counts.",
231
+ inputSchema: {
232
+ type: "object",
233
+ properties: {},
234
+ },
235
+ },
236
+ {
237
+ name: "recommend_skills",
238
+ description: "Get skill recommendations based on what you are working on. Describe your project or task and get relevant skill suggestions.",
239
+ inputSchema: {
240
+ type: "object",
241
+ properties: {
242
+ context: {
243
+ type: "string",
244
+ description: "Description of what you are working on. Example: 'building a Next.js app with Supabase and Stripe'",
245
+ },
246
+ installed: {
247
+ type: "string",
248
+ description: "Comma-separated slugs of already-installed skills to avoid duplicates",
249
+ },
250
+ },
251
+ required: ["context"],
252
+ },
253
+ },
254
+ {
255
+ name: "check_capability_gap",
256
+ 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.",
257
+ inputSchema: {
258
+ type: "object",
259
+ properties: {
260
+ task: {
261
+ type: "string",
262
+ description: "What the agent is trying to do",
263
+ },
264
+ current_tools: {
265
+ type: "array",
266
+ items: { type: "string" },
267
+ description: "Tools the agent already has",
268
+ },
269
+ agent_type: {
270
+ type: "string",
271
+ enum: ["claude-code", "cursor", "codex-cli", "windsurf"],
272
+ description: "The agent platform being used",
273
+ },
274
+ },
275
+ required: ["task", "current_tools", "agent_type"],
276
+ },
277
+ },
278
+ {
279
+ name: "report_skill_usage",
280
+ description: "Report whether an installed skill worked correctly. Helps improve quality scores.",
281
+ inputSchema: {
282
+ type: "object",
283
+ properties: {
284
+ slug: {
285
+ type: "string",
286
+ description: "Skill slug in owner/repo format",
287
+ },
288
+ status: {
289
+ type: "string",
290
+ enum: ["success", "error", "partial"],
291
+ description: "Whether the skill worked correctly",
292
+ },
293
+ error_message: {
294
+ type: "string",
295
+ description: "Error details if the skill failed or partially worked",
296
+ },
297
+ },
298
+ required: ["slug", "status"],
299
+ },
300
+ },
301
+ {
302
+ name: "list_my_proofs",
303
+ description: "List all execution proofs for this agent. Shows your verified skill usage history -- your agent resume.",
304
+ inputSchema: {
305
+ type: "object",
306
+ properties: {},
307
+ },
308
+ },
309
+ {
310
+ name: "verify_proof",
311
+ description: "Verify an execution proof by its proof ID. Confirms whether a proof is valid and which skill it covers.",
312
+ inputSchema: {
313
+ type: "object",
314
+ properties: {
315
+ proof_id: {
316
+ type: "string",
317
+ description: "The proof ID to verify. Example: 'lp_a1b2c3d4e5f6g7h8'",
318
+ },
319
+ },
320
+ required: ["proof_id"],
321
+ },
322
+ },
323
+ {
324
+ name: "save_memory",
325
+ 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.",
326
+ inputSchema: {
327
+ type: "object",
328
+ properties: {
329
+ key: {
330
+ type: "string",
331
+ description: "Memory key name. Examples: 'installed_skills', 'preferred_categories', 'project_context'",
332
+ },
333
+ value: {
334
+ description: "Any JSON-serializable value to store (string, array, object, number, etc.)",
335
+ },
336
+ type: {
337
+ type: "string",
338
+ enum: ["search", "install", "preference", "context"],
339
+ description: "Category of this memory entry",
340
+ },
341
+ },
342
+ required: ["key", "value", "type"],
343
+ },
344
+ },
345
+ {
346
+ name: "recall_memory",
347
+ 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.",
348
+ inputSchema: {
349
+ type: "object",
350
+ properties: {
351
+ type: {
352
+ type: "string",
353
+ enum: ["search", "install", "preference", "context"],
354
+ description: "Filter memories by type. Omit to get all memories.",
355
+ },
356
+ },
357
+ },
358
+ },
359
+ {
360
+ name: "request_permission",
361
+ 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.",
362
+ inputSchema: {
363
+ type: "object",
364
+ properties: {
365
+ slug: {
366
+ type: "string",
367
+ description: "Skill slug in owner/repo format",
368
+ },
369
+ reason: {
370
+ type: "string",
371
+ description: "Why the agent wants this skill. Example: 'I need database access to complete the migration task'",
372
+ },
373
+ },
374
+ required: ["slug", "reason"],
375
+ },
376
+ },
377
+ {
378
+ name: "check_permission",
379
+ description: "Check the status of a previously submitted permission request. Returns 'pending', 'approved', or 'denied'.",
380
+ inputSchema: {
381
+ type: "object",
382
+ properties: {
383
+ request_id: {
384
+ type: "number",
385
+ description: "The request ID returned from request_permission",
386
+ },
387
+ },
388
+ required: ["request_id"],
389
+ },
390
+ },
391
+ {
392
+ name: "install_batch",
393
+ description: "Install multiple skills at once. Returns install configs for all requested skills in a single call.",
394
+ inputSchema: {
395
+ type: "object",
396
+ properties: {
397
+ slugs: {
398
+ type: "array",
399
+ items: { type: "string" },
400
+ description: "Array of skill slugs in owner/repo format. Maximum 20.",
401
+ },
402
+ agent: {
403
+ type: "string",
404
+ enum: ["claude-code", "cursor", "codex-cli", "windsurf", "generic"],
405
+ description: "Target agent platform",
406
+ },
407
+ },
408
+ required: ["slugs", "agent"],
409
+ },
410
+ },
411
+ {
412
+ name: "share_loadout",
413
+ description: "Get this agent's public loadout (installed skills, trust score, stats). Use this to share your current skill configuration.",
414
+ inputSchema: {
415
+ type: "object",
416
+ properties: {},
417
+ },
418
+ },
419
+ {
420
+ name: "install_pack",
421
+ 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.",
422
+ inputSchema: {
423
+ type: "object",
424
+ properties: {
425
+ slug: {
426
+ type: "string",
427
+ description: "Pack slug. Examples: 'research-agent', 'full-stack-developer', 'devops-engineer', 'data-engineer', 'browser-automation'",
428
+ },
429
+ agent: {
430
+ type: "string",
431
+ enum: ["claude-code", "cursor", "codex-cli", "windsurf", "generic"],
432
+ description: "Target agent platform",
433
+ },
434
+ },
435
+ required: ["slug", "agent"],
436
+ },
437
+ },
438
+ {
439
+ name: "validate_action",
440
+ description: "Validate whether an action on a skill is safe before executing it. Checks security grade, safety manifest, parameter injection, and skill freshness.",
441
+ inputSchema: {
442
+ type: "object",
443
+ properties: {
444
+ slug: {
445
+ type: "string",
446
+ description: "Skill slug in owner/repo format. Example: 'supabase/mcp'",
447
+ },
448
+ action: {
449
+ type: "string",
450
+ description: "The action about to be performed. Example: 'query_database'",
451
+ },
452
+ parameters: {
453
+ type: "object",
454
+ description: "Parameters that will be passed to the action. Checked for injection patterns.",
455
+ },
456
+ },
457
+ required: ["slug", "action"],
458
+ },
459
+ },
460
+ {
461
+ name: "flag_skill",
462
+ description: "Report a skill that behaves unexpectedly, contains prompt injection, is broken, or is otherwise problematic. Helps the community identify unsafe skills.",
463
+ inputSchema: {
464
+ type: "object",
465
+ properties: {
466
+ slug: {
467
+ type: "string",
468
+ description: "Skill slug in owner/repo format. Example: 'owner/repo-name'",
469
+ },
470
+ reason: {
471
+ type: "string",
472
+ enum: [
473
+ "prompt_injection",
474
+ "malicious",
475
+ "broken",
476
+ "misleading",
477
+ "spam",
478
+ "other",
479
+ ],
480
+ description: "Why the skill is being flagged",
481
+ },
482
+ details: {
483
+ type: "string",
484
+ description: "Additional context about the issue. Example: 'The SKILL.md contains instructions to ignore safety checks'",
485
+ },
486
+ },
487
+ required: ["slug", "reason"],
488
+ },
489
+ },
490
+ {
491
+ name: "review_skill",
492
+ description: "Leave a review for a skill you have used. Helps other agents and humans decide whether to install it.",
493
+ inputSchema: {
494
+ type: "object",
495
+ properties: {
496
+ slug: {
497
+ type: "string",
498
+ description: "Skill slug in owner/repo format. Example: 'supabase/mcp'",
499
+ },
500
+ rating: {
501
+ type: "number",
502
+ description: "Rating from 1 to 5. 5 = excellent, 1 = unusable.",
503
+ },
504
+ comment: {
505
+ type: "string",
506
+ description: "Optional comment about your experience. Example: 'Works great for database queries, fast and reliable'",
507
+ },
508
+ },
509
+ required: ["slug", "rating"],
510
+ },
511
+ },
512
+ {
513
+ name: "set_profile",
514
+ description: "Set your agent's public profile display name and bio. This information appears on your public profile page at loaditout.ai/agents/{key}.",
515
+ inputSchema: {
516
+ type: "object",
517
+ properties: {
518
+ display_name: {
519
+ type: "string",
520
+ description: "Display name for your agent profile (max 100 characters). Example: 'Full-Stack Dev Agent'",
521
+ },
522
+ bio: {
523
+ type: "string",
524
+ description: "Short bio for your agent profile (max 500 characters). Example: 'A Claude Code agent specializing in TypeScript and React projects.'",
525
+ },
526
+ },
527
+ },
528
+ },
529
+ {
530
+ name: "smart_search",
531
+ description: "Search for skills with your history and preferences automatically applied. Returns personalized results excluding skills you already have. Use this by default instead of search_skills.",
532
+ inputSchema: {
533
+ type: "object",
534
+ properties: {
535
+ query: {
536
+ type: "string",
537
+ description: "Natural language search query. Examples: 'postgres database', 'browser automation', 'github issues'",
538
+ },
539
+ limit: {
540
+ type: "number",
541
+ description: "Max results to return (default 10, max 25)",
542
+ },
543
+ },
544
+ required: ["query"],
545
+ },
546
+ },
547
+ ];
548
+ /**
549
+ * Load agent memory entries. Returns parsed memories or empty array.
550
+ * Never throws -- failures are silently ignored so callers always proceed.
551
+ */
552
+ async function loadAgentMemory(typeFilter) {
553
+ try {
554
+ const params = new URLSearchParams({ agent_key: AGENT_KEY });
555
+ if (typeFilter)
556
+ params.set("type", typeFilter);
557
+ const result = (await fetchJSON(`${API_BASE}/memory?${params.toString()}`));
558
+ return result.memories ?? [];
559
+ }
560
+ catch {
561
+ return [];
562
+ }
563
+ }
564
+ /**
565
+ * Extract installed skill slugs from memory entries.
566
+ */
567
+ function extractInstalledSlugs(memories) {
568
+ for (const m of memories) {
569
+ if (m.key === "installed_skills" && Array.isArray(m.value)) {
570
+ return m.value;
571
+ }
572
+ }
573
+ return [];
574
+ }
575
+ /**
576
+ * Extract recent search queries from memory entries.
577
+ */
578
+ function extractRecentSearches(memories) {
579
+ for (const m of memories) {
580
+ if (m.key === "recent_searches" && Array.isArray(m.value)) {
581
+ return m.value;
582
+ }
583
+ }
584
+ return [];
585
+ }
586
+ /**
587
+ * Fire-and-forget save to agent memory. Never throws.
588
+ */
589
+ function saveMemoryAsync(key, value, type) {
590
+ postJSON(`${API_BASE}/memory`, {
591
+ agent_key: AGENT_KEY,
592
+ key,
593
+ value,
594
+ type,
595
+ }).catch(() => { });
596
+ }
597
+ /**
598
+ * Append a search query to the recent_searches memory (keep last 20).
599
+ */
600
+ function recordSearchQuery(query, existingSearches) {
601
+ const updated = [...existingSearches.filter((q) => q !== query), query].slice(-20);
602
+ saveMemoryAsync("recent_searches", updated, "search");
603
+ }
604
+ // --- Tool handlers ---
605
+ async function handleSearchSkills(args) {
606
+ // Auto-load agent memory to get installed skills and search history
607
+ const memories = await loadAgentMemory();
608
+ const installedSlugs = extractInstalledSlugs(memories);
609
+ const recentSearches = extractRecentSearches(memories);
610
+ const params = new URLSearchParams({ q: args.query });
611
+ if (args.type)
612
+ params.set("type", args.type);
613
+ if (args.agent)
614
+ params.set("agent", args.agent);
615
+ if (args.limit)
616
+ params.set("limit", String(args.limit));
617
+ const result = await fetchJSON(`${API_BASE}/search?${params.toString()}`, {
618
+ "X-Agent-Key": AGENT_KEY,
619
+ });
620
+ // Fire-and-forget: record this search query to memory
621
+ recordSearchQuery(args.query, recentSearches);
622
+ // Filter out already-installed skills from results client-side
623
+ const parsed = result;
624
+ if (parsed.results && installedSlugs.length > 0) {
625
+ const installedSet = new Set(installedSlugs);
626
+ parsed.results = parsed.results.filter((r) => !r.slug || !installedSet.has(r.slug));
627
+ }
628
+ return JSON.stringify(parsed, null, 2);
629
+ }
630
+ async function handleGetSkill(args) {
631
+ const result = await fetchJSON(`${API_BASE}/skill/${encodeURIComponent(args.slug)}`);
632
+ return JSON.stringify(result, null, 2);
633
+ }
634
+ async function handleInstallSkill(args) {
635
+ const params = new URLSearchParams({ agent: args.agent });
636
+ const result = await fetchJSON(`${API_BASE}/install/${encodeURIComponent(args.slug)}?${params.toString()}`, { "X-Agent-Key": AGENT_KEY });
637
+ // Fire-and-forget: save this slug to installed_skills memory
638
+ const memories = await loadAgentMemory().catch(() => []);
639
+ const existing = extractInstalledSlugs(memories);
640
+ if (!existing.includes(args.slug)) {
641
+ saveMemoryAsync("installed_skills", [...existing, args.slug], "install");
642
+ }
643
+ return JSON.stringify(result, null, 2);
644
+ }
645
+ async function handleListCategories() {
646
+ const result = await fetchJSON(`${API_BASE}/categories`);
647
+ return JSON.stringify(result, null, 2);
648
+ }
649
+ async function handleRecommendSkills(args) {
650
+ // Auto-load agent memory for personalization
651
+ const memories = await loadAgentMemory();
652
+ const installedSlugs = extractInstalledSlugs(memories);
653
+ const recentSearches = extractRecentSearches(memories);
654
+ // Merge installed skills from memory with any explicitly provided
655
+ const allInstalled = new Set(installedSlugs);
656
+ if (args.installed) {
657
+ for (const s of args.installed.split(",").map((s) => s.trim()).filter(Boolean)) {
658
+ allInstalled.add(s);
659
+ }
660
+ }
661
+ // Enrich context with relevant search history keywords
662
+ let enrichedContext = args.context;
663
+ if (recentSearches.length > 0) {
664
+ const recentKeywords = recentSearches.slice(-5).join(", ");
665
+ enrichedContext = `${args.context} (recent interests: ${recentKeywords})`;
666
+ }
667
+ const params = new URLSearchParams({ context: enrichedContext });
668
+ if (allInstalled.size > 0) {
669
+ params.set("installed", Array.from(allInstalled).join(","));
670
+ }
671
+ const result = await fetchJSON(`${API_BASE}/recommend?${params.toString()}`, { "X-Agent-Key": AGENT_KEY });
672
+ return JSON.stringify(result, null, 2);
673
+ }
674
+ async function handleCheckCapabilityGap(args) {
675
+ const params = new URLSearchParams({
676
+ context: args.task,
677
+ installed: args.current_tools.join(","),
678
+ });
679
+ const result = (await fetchJSON(`${API_BASE}/recommend?${params.toString()}`));
680
+ const recommendations = result.results ?? [];
681
+ if (recommendations.length === 0) {
682
+ return "No additional skills needed for this task.";
683
+ }
684
+ const lines = ["Based on what you're doing, these skills might help:\n"];
685
+ recommendations.forEach((r, i) => {
686
+ const name = r.name ?? r.slug ?? "unknown";
687
+ const score = r.quality_score ?? 0;
688
+ const desc = r.description ?? "";
689
+ lines.push(`${i + 1}. ${r.slug} (quality: ${score}) - ${desc}`);
690
+ lines.push(` Install: npx loaditout add ${r.slug}`);
691
+ });
692
+ return lines.join("\n");
693
+ }
694
+ async function handleReportSkillUsage(args) {
695
+ const body = {
696
+ slug: args.slug,
697
+ status: args.status,
698
+ agent: "claude-code",
699
+ agent_key: AGENT_KEY,
700
+ };
701
+ if (args.error_message) {
702
+ body.error_message = args.error_message;
703
+ }
704
+ const result = (await postJSON(`${API_BASE}/report`, body));
705
+ if (result.proof) {
706
+ const shareUrl = `https://loaditout.ai/proof/${result.proof.proof_id}`;
707
+ const lines = [
708
+ "Skill usage reported successfully.",
709
+ "",
710
+ "Execution Proof:",
711
+ ` Proof ID: ${result.proof.proof_id}`,
712
+ ` Verify: ${result.proof.verify_url}`,
713
+ ` Share: ${shareUrl}`,
714
+ ` ${result.proof.shareable_text}`,
715
+ ];
716
+ return lines.join("\n");
717
+ }
718
+ return JSON.stringify(result, null, 2);
719
+ }
720
+ async function handleListMyProofs() {
721
+ const params = new URLSearchParams({ agent_key: AGENT_KEY });
722
+ const result = (await fetchJSON(`${API_BASE}/proofs?${params.toString()}`));
723
+ if (result.error) {
724
+ return `Failed to list proofs: ${result.error}`;
725
+ }
726
+ const proofs = result.proofs ?? [];
727
+ if (proofs.length === 0) {
728
+ return "No execution proofs yet. Report successful skill usage to earn proofs.";
729
+ }
730
+ const lines = [
731
+ `Execution Proofs (${proofs.length} total):`,
732
+ "",
733
+ ];
734
+ proofs.forEach((p, i) => {
735
+ lines.push(`${i + 1}. ${p.skill_name ?? p.skill_slug} [${p.proof_id}]`);
736
+ lines.push(` Skill: ${p.skill_slug}`);
737
+ lines.push(` Verify: ${p.verify_url}`);
738
+ lines.push(` Share: https://loaditout.ai/proof/${p.proof_id}`);
739
+ lines.push(` Date: ${p.created_at ?? "unknown"}`);
740
+ lines.push("");
741
+ });
742
+ return lines.join("\n");
743
+ }
744
+ async function handleVerifyProof(args) {
745
+ const result = (await fetchJSON(`${API_BASE}/verify/${encodeURIComponent(args.proof_id)}`));
746
+ if (result.error) {
747
+ return `Verification failed: ${result.error}`;
748
+ }
749
+ if (!result.valid) {
750
+ return `Proof ${args.proof_id} is NOT valid.`;
751
+ }
752
+ return [
753
+ `Proof ${result.proof_id} is VALID.`,
754
+ ` Skill: ${result.skill}`,
755
+ ` Verified at: ${result.verified_at}`,
756
+ ` Platform: ${result.platform}`,
757
+ ].join("\n");
758
+ }
759
+ async function handleSaveMemory(args) {
760
+ const body = {
761
+ agent_key: AGENT_KEY,
762
+ key: args.key,
763
+ value: args.value,
764
+ type: args.type,
765
+ };
766
+ const result = await postJSON(`${API_BASE}/memory`, body);
767
+ return JSON.stringify(result, null, 2);
768
+ }
769
+ async function handleRecallMemory(args) {
770
+ const params = new URLSearchParams({ agent_key: AGENT_KEY });
771
+ if (args.type)
772
+ params.set("type", args.type);
773
+ const result = await fetchJSON(`${API_BASE}/memory?${params.toString()}`);
774
+ return JSON.stringify(result, null, 2);
775
+ }
776
+ async function handleRequestPermission(args) {
777
+ const body = {
778
+ agent_key: AGENT_KEY,
779
+ slug: args.slug,
780
+ reason: args.reason,
781
+ };
782
+ const result = (await postJSON(`${API_BASE}/permission`, body));
783
+ if (result.error) {
784
+ return `Permission request failed: ${result.error}`;
785
+ }
786
+ return `Permission requested (ID: ${result.request_id}). Status: ${result.status}. Waiting for human approval.`;
787
+ }
788
+ async function handleCheckPermission(args) {
789
+ const params = new URLSearchParams({
790
+ agent_key: AGENT_KEY,
791
+ request_id: String(args.request_id),
792
+ });
793
+ const result = (await fetchJSON(`${API_BASE}/permission?${params.toString()}`));
794
+ if (result.error) {
795
+ return `Failed to check permission: ${result.error}`;
796
+ }
797
+ return `Permission status: ${result.status}`;
798
+ }
799
+ async function handleInstallBatch(args) {
800
+ const body = {
801
+ slugs: args.slugs,
802
+ agent: args.agent,
803
+ };
804
+ const result = await postJSON(`${API_BASE}/install-batch`, body);
805
+ return JSON.stringify(result, null, 2);
806
+ }
807
+ async function handleShareLoadout() {
808
+ const result = await fetchJSON(`${API_BASE}/loadout/${encodeURIComponent(AGENT_KEY)}`);
809
+ const parsed = result;
810
+ parsed.profile_url = `https://loaditout.ai/agents/${AGENT_KEY}`;
811
+ return JSON.stringify(parsed, null, 2);
812
+ }
813
+ async function handleSetProfile(args) {
814
+ const body = {
815
+ agent_key: AGENT_KEY,
816
+ };
817
+ if (args.display_name !== undefined)
818
+ body.display_name = args.display_name;
819
+ if (args.bio !== undefined)
820
+ body.bio = args.bio;
821
+ const result = (await postJSON(`${API_BASE}/profile`, body));
822
+ if (result.error) {
823
+ return `Failed to update profile: ${result.error}`;
824
+ }
825
+ const lines = [
826
+ "Profile updated successfully.",
827
+ ` Trust score: ${result.trust_score}`,
828
+ ` Profile: https://loaditout.ai/agents/${AGENT_KEY}`,
829
+ ];
830
+ return lines.join("\n");
831
+ }
832
+ async function handleInstallPack(args) {
833
+ const params = new URLSearchParams({ agent: args.agent });
834
+ const result = (await fetchJSON(`${API_BASE}/pack/${encodeURIComponent(args.slug)}?${params.toString()}`));
835
+ if (result.error) {
836
+ return `Pack not found: ${result.error}`;
837
+ }
838
+ const packSkills = result.skills ?? [];
839
+ const lines = [
840
+ `Agent Pack: ${result.name ?? result.slug}`,
841
+ `Outcome: ${result.target_outcome ?? "N/A"}`,
842
+ `Skills: ${packSkills.length}`,
843
+ `Install all: ${result.install_all_command ?? `npx loaditout add-pack ${args.slug}`}`,
844
+ "",
845
+ "Skills in this pack:",
846
+ "",
847
+ ];
848
+ packSkills.forEach((s, i) => {
849
+ lines.push(`${i + 1}. ${s.name ?? s.slug} (${s.type ?? "unknown"})`);
850
+ lines.push(` Slug: ${s.slug}`);
851
+ if (s.install_config) {
852
+ lines.push(` Config: ${JSON.stringify(s.install_config)}`);
853
+ }
854
+ lines.push("");
855
+ });
856
+ return lines.join("\n");
857
+ }
858
+ async function handleSmartSearch(args) {
859
+ // Load all agent memory (installed skills, recent searches, preferences)
860
+ const memories = await loadAgentMemory();
861
+ const installedSlugs = extractInstalledSlugs(memories);
862
+ const recentSearches = extractRecentSearches(memories);
863
+ const params = new URLSearchParams({ q: args.query });
864
+ if (args.limit)
865
+ params.set("limit", String(args.limit));
866
+ const result = await fetchJSON(`${API_BASE}/search?${params.toString()}`, {
867
+ "X-Agent-Key": AGENT_KEY,
868
+ });
869
+ // Fire-and-forget: record this search query
870
+ recordSearchQuery(args.query, recentSearches);
871
+ // Filter out already-installed skills
872
+ const parsed = result;
873
+ const excludedCount = { value: 0 };
874
+ if (parsed.results && installedSlugs.length > 0) {
875
+ const installedSet = new Set(installedSlugs);
876
+ const originalLength = parsed.results.length;
877
+ parsed.results = parsed.results.filter((r) => !r.slug || !installedSet.has(r.slug));
878
+ excludedCount.value = originalLength - parsed.results.length;
879
+ }
880
+ // Add metadata about personalization
881
+ const output = { ...parsed };
882
+ output._personalization = {
883
+ installed_skills_excluded: excludedCount.value,
884
+ installed_skills_count: installedSlugs.length,
885
+ recent_searches: recentSearches.slice(-5),
886
+ };
887
+ return JSON.stringify(output, null, 2);
888
+ }
889
+ async function handleFlagSkill(args) {
890
+ const body = {
891
+ reason: args.reason,
892
+ };
893
+ if (args.details) {
894
+ body.details = args.details;
895
+ }
896
+ const url = `https://loaditout.ai/api/skills/${encodeURIComponent(args.slug)}/flag`;
897
+ const result = (await postJSON(url, body, { "X-Agent-Key": AGENT_KEY }));
898
+ if (result.error) {
899
+ return `Flag failed: ${result.error}`;
900
+ }
901
+ return `Skill "${args.slug}" has been flagged for "${args.reason}". Thank you for helping keep the registry safe.`;
902
+ }
903
+ async function handleReviewSkill(args) {
904
+ const body = {
905
+ agent_key: AGENT_KEY,
906
+ slug: args.slug,
907
+ rating: args.rating,
908
+ };
909
+ if (args.comment) {
910
+ body.comment = args.comment;
911
+ }
912
+ const result = (await postJSON(`${API_BASE}/review`, body));
913
+ if (result.error) {
914
+ return `Review failed: ${result.error}`;
915
+ }
916
+ return `Review submitted for "${args.slug}" (${args.rating}/5). Thank you for your feedback.`;
917
+ }
918
+ async function handleValidateAction(args) {
919
+ const body = {
920
+ slug: args.slug,
921
+ action: args.action,
922
+ agent_key: AGENT_KEY,
923
+ };
924
+ if (args.parameters) {
925
+ body.parameters = args.parameters;
926
+ }
927
+ const result = (await postJSON(`${API_BASE}/validate`, body));
928
+ if (result.error) {
929
+ return `Validation failed: ${result.error}`;
930
+ }
931
+ const lines = [
932
+ `Validation result for ${args.slug} / ${args.action}:`,
933
+ ` Safe to proceed: ${result.valid ? "YES" : "NO"}`,
934
+ ` Risk level: ${result.risk_level ?? "unknown"}`,
935
+ ` Security grade: ${result.security_grade ?? "unknown"}`,
936
+ ` Skill verified: ${result.skill_verified ? "yes" : "no"}`,
937
+ ];
938
+ if (result.last_updated_days_ago !== undefined) {
939
+ lines.push(` Last updated: ${result.last_updated_days_ago} days ago`);
940
+ }
941
+ const warnings = result.warnings ?? [];
942
+ if (warnings.length > 0) {
943
+ lines.push(` Warnings:`);
944
+ for (const w of warnings) {
945
+ lines.push(` - ${w}`);
946
+ }
947
+ }
948
+ return lines.join("\n");
949
+ }
950
+ function makeResponse(id, result) {
951
+ return { jsonrpc: "2.0", id, result };
952
+ }
953
+ function makeError(id, code, message) {
954
+ return { jsonrpc: "2.0", id, error: { code, message } };
955
+ }
956
+ function send(response) {
957
+ const json = JSON.stringify(response);
958
+ process.stdout.write(json + "\n");
959
+ }
960
+ async function handleRequest(request) {
961
+ const { id, method, params } = request;
962
+ switch (method) {
963
+ case "initialize": {
964
+ send(makeResponse(id, {
965
+ protocolVersion: "2024-11-05",
966
+ capabilities: {
967
+ tools: {},
968
+ resources: {},
969
+ },
970
+ serverInfo: {
971
+ name: SERVER_NAME,
972
+ version: SERVER_VERSION,
973
+ },
974
+ }));
975
+ break;
976
+ }
977
+ case "notifications/initialized": {
978
+ // No response needed for notifications
979
+ break;
980
+ }
981
+ case "tools/list": {
982
+ send(makeResponse(id, { tools: TOOLS }));
983
+ break;
984
+ }
985
+ case "tools/call": {
986
+ const toolName = params?.name;
987
+ const toolArgs = (params
988
+ ?.arguments || {});
989
+ try {
990
+ let resultText;
991
+ switch (toolName) {
992
+ case "search_skills":
993
+ resultText = await handleSearchSkills(toolArgs);
994
+ break;
995
+ case "get_skill":
996
+ resultText = await handleGetSkill(toolArgs);
997
+ break;
998
+ case "install_skill":
999
+ resultText = await handleInstallSkill(toolArgs);
1000
+ break;
1001
+ case "list_categories":
1002
+ resultText = await handleListCategories();
1003
+ break;
1004
+ case "recommend_skills":
1005
+ resultText = await handleRecommendSkills(toolArgs);
1006
+ break;
1007
+ case "check_capability_gap":
1008
+ resultText = await handleCheckCapabilityGap(toolArgs);
1009
+ break;
1010
+ case "report_skill_usage":
1011
+ resultText = await handleReportSkillUsage(toolArgs);
1012
+ break;
1013
+ case "list_my_proofs":
1014
+ resultText = await handleListMyProofs();
1015
+ break;
1016
+ case "verify_proof":
1017
+ resultText = await handleVerifyProof(toolArgs);
1018
+ break;
1019
+ case "save_memory":
1020
+ resultText = await handleSaveMemory(toolArgs);
1021
+ break;
1022
+ case "recall_memory":
1023
+ resultText = await handleRecallMemory(toolArgs);
1024
+ break;
1025
+ case "request_permission":
1026
+ resultText = await handleRequestPermission(toolArgs);
1027
+ break;
1028
+ case "check_permission":
1029
+ resultText = await handleCheckPermission(toolArgs);
1030
+ break;
1031
+ case "install_batch":
1032
+ resultText = await handleInstallBatch(toolArgs);
1033
+ break;
1034
+ case "share_loadout":
1035
+ resultText = await handleShareLoadout();
1036
+ break;
1037
+ case "set_profile":
1038
+ resultText = await handleSetProfile(toolArgs);
1039
+ break;
1040
+ case "install_pack":
1041
+ resultText = await handleInstallPack(toolArgs);
1042
+ break;
1043
+ case "validate_action":
1044
+ resultText = await handleValidateAction(toolArgs);
1045
+ break;
1046
+ case "flag_skill":
1047
+ resultText = await handleFlagSkill(toolArgs);
1048
+ break;
1049
+ case "review_skill":
1050
+ resultText = await handleReviewSkill(toolArgs);
1051
+ break;
1052
+ case "smart_search":
1053
+ resultText = await handleSmartSearch(toolArgs);
1054
+ break;
1055
+ default:
1056
+ send(makeError(id, -32601, `Unknown tool: ${toolName}`));
1057
+ return;
1058
+ }
1059
+ send(makeResponse(id, {
1060
+ content: [{ type: "text", text: resultText }],
1061
+ }));
1062
+ }
1063
+ catch (err) {
1064
+ const message = err instanceof Error ? err.message : String(err);
1065
+ send(makeResponse(id, {
1066
+ content: [{ type: "text", text: `Error: ${message}` }],
1067
+ isError: true,
1068
+ }));
1069
+ }
1070
+ break;
1071
+ }
1072
+ case "resources/list": {
1073
+ send(makeResponse(id, {
1074
+ resources: [
1075
+ {
1076
+ uri: "loaditout://catalog",
1077
+ name: "Loaditout Skill Catalog",
1078
+ description: "Browse the full Loaditout skill catalog by category",
1079
+ mimeType: "text/plain",
1080
+ },
1081
+ ],
1082
+ }));
1083
+ break;
1084
+ }
1085
+ case "resources/read": {
1086
+ const uri = params?.uri;
1087
+ if (uri === "loaditout://catalog") {
1088
+ try {
1089
+ const categories = (await fetchJSON(`${API_BASE}/categories`));
1090
+ const cats = categories.categories ?? [];
1091
+ const lines = ["Loaditout Skill Catalog\n"];
1092
+ for (const cat of cats) {
1093
+ const name = cat.name ?? cat.slug ?? "unknown";
1094
+ const count = cat.count ?? 0;
1095
+ const desc = cat.description ?? "";
1096
+ lines.push(`- ${name} (${count} skills): ${desc}`);
1097
+ }
1098
+ send(makeResponse(id, {
1099
+ contents: [
1100
+ {
1101
+ uri: "loaditout://catalog",
1102
+ mimeType: "text/plain",
1103
+ text: lines.join("\n"),
1104
+ },
1105
+ ],
1106
+ }));
1107
+ }
1108
+ catch (err) {
1109
+ const message = err instanceof Error ? err.message : String(err);
1110
+ send(makeError(id, -32603, `Failed to load catalog: ${message}`));
1111
+ }
1112
+ }
1113
+ else {
1114
+ send(makeError(id, -32602, `Unknown resource: ${uri}`));
1115
+ }
1116
+ break;
1117
+ }
1118
+ case "ping": {
1119
+ send(makeResponse(id, {}));
1120
+ break;
1121
+ }
1122
+ default: {
1123
+ if (id !== undefined && id !== null) {
1124
+ send(makeError(id, -32601, `Method not found: ${method}`));
1125
+ }
1126
+ break;
1127
+ }
1128
+ }
1129
+ }
1130
+ // --- Main: read JSON-RPC messages from stdin ---
1131
+ function main() {
1132
+ const rl = readline.createInterface({
1133
+ input: process.stdin,
1134
+ terminal: false,
1135
+ });
1136
+ rl.on("line", (line) => {
1137
+ const trimmed = line.trim();
1138
+ if (!trimmed)
1139
+ return;
1140
+ let request;
1141
+ try {
1142
+ request = JSON.parse(trimmed);
1143
+ }
1144
+ catch {
1145
+ send(makeError(null, -32700, "Parse error"));
1146
+ return;
1147
+ }
1148
+ handleRequest(request).catch((err) => {
1149
+ send(makeError(request.id ?? null, -32603, err instanceof Error ? err.message : String(err)));
1150
+ });
1151
+ });
1152
+ rl.on("close", () => {
1153
+ process.exit(0);
1154
+ });
1155
+ }
1156
+ main();