loaditout-mcp-server 0.1.0 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +337 -7
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -101,7 +101,7 @@ function fetchJSON(url, extraHeaders) {
101
101
  .on("error", reject);
102
102
  });
103
103
  }
104
- function postJSON(url, body) {
104
+ function postJSON(url, body, extraHeaders) {
105
105
  return new Promise((resolve, reject) => {
106
106
  const payload = JSON.stringify(body);
107
107
  const parsed = new URL(url);
@@ -114,6 +114,7 @@ function postJSON(url, body) {
114
114
  "Content-Type": "application/json",
115
115
  "Content-Length": Buffer.byteLength(payload),
116
116
  "User-Agent": `loaditout-mcp/${SERVER_VERSION}`,
117
+ ...extraHeaders,
117
118
  },
118
119
  };
119
120
  const req = https.request(options, (res) => {
@@ -297,6 +298,28 @@ const TOOLS = [
297
298
  required: ["slug", "status"],
298
299
  },
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
+ },
300
323
  {
301
324
  name: "save_memory",
302
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.",
@@ -412,9 +435,139 @@ const TOOLS = [
412
435
  required: ["slug", "agent"],
413
436
  },
414
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: "smart_search",
492
+ 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.",
493
+ inputSchema: {
494
+ type: "object",
495
+ properties: {
496
+ query: {
497
+ type: "string",
498
+ description: "Natural language search query. Examples: 'postgres database', 'browser automation', 'github issues'",
499
+ },
500
+ limit: {
501
+ type: "number",
502
+ description: "Max results to return (default 10, max 25)",
503
+ },
504
+ },
505
+ required: ["query"],
506
+ },
507
+ },
415
508
  ];
509
+ /**
510
+ * Load agent memory entries. Returns parsed memories or empty array.
511
+ * Never throws -- failures are silently ignored so callers always proceed.
512
+ */
513
+ async function loadAgentMemory(typeFilter) {
514
+ try {
515
+ const params = new URLSearchParams({ agent_key: AGENT_KEY });
516
+ if (typeFilter)
517
+ params.set("type", typeFilter);
518
+ const result = (await fetchJSON(`${API_BASE}/memory?${params.toString()}`));
519
+ return result.memories ?? [];
520
+ }
521
+ catch {
522
+ return [];
523
+ }
524
+ }
525
+ /**
526
+ * Extract installed skill slugs from memory entries.
527
+ */
528
+ function extractInstalledSlugs(memories) {
529
+ for (const m of memories) {
530
+ if (m.key === "installed_skills" && Array.isArray(m.value)) {
531
+ return m.value;
532
+ }
533
+ }
534
+ return [];
535
+ }
536
+ /**
537
+ * Extract recent search queries from memory entries.
538
+ */
539
+ function extractRecentSearches(memories) {
540
+ for (const m of memories) {
541
+ if (m.key === "recent_searches" && Array.isArray(m.value)) {
542
+ return m.value;
543
+ }
544
+ }
545
+ return [];
546
+ }
547
+ /**
548
+ * Fire-and-forget save to agent memory. Never throws.
549
+ */
550
+ function saveMemoryAsync(key, value, type) {
551
+ postJSON(`${API_BASE}/memory`, {
552
+ agent_key: AGENT_KEY,
553
+ key,
554
+ value,
555
+ type,
556
+ }).catch(() => { });
557
+ }
558
+ /**
559
+ * Append a search query to the recent_searches memory (keep last 20).
560
+ */
561
+ function recordSearchQuery(query, existingSearches) {
562
+ const updated = [...existingSearches.filter((q) => q !== query), query].slice(-20);
563
+ saveMemoryAsync("recent_searches", updated, "search");
564
+ }
416
565
  // --- Tool handlers ---
417
566
  async function handleSearchSkills(args) {
567
+ // Auto-load agent memory to get installed skills and search history
568
+ const memories = await loadAgentMemory();
569
+ const installedSlugs = extractInstalledSlugs(memories);
570
+ const recentSearches = extractRecentSearches(memories);
418
571
  const params = new URLSearchParams({ q: args.query });
419
572
  if (args.type)
420
573
  params.set("type", args.type);
@@ -422,8 +575,18 @@ async function handleSearchSkills(args) {
422
575
  params.set("agent", args.agent);
423
576
  if (args.limit)
424
577
  params.set("limit", String(args.limit));
425
- const result = await fetchJSON(`${API_BASE}/search?${params.toString()}`);
426
- return JSON.stringify(result, null, 2);
578
+ const result = await fetchJSON(`${API_BASE}/search?${params.toString()}`, {
579
+ "X-Agent-Key": AGENT_KEY,
580
+ });
581
+ // Fire-and-forget: record this search query to memory
582
+ recordSearchQuery(args.query, recentSearches);
583
+ // Filter out already-installed skills from results client-side
584
+ const parsed = result;
585
+ if (parsed.results && installedSlugs.length > 0) {
586
+ const installedSet = new Set(installedSlugs);
587
+ parsed.results = parsed.results.filter((r) => !r.slug || !installedSet.has(r.slug));
588
+ }
589
+ return JSON.stringify(parsed, null, 2);
427
590
  }
428
591
  async function handleGetSkill(args) {
429
592
  const result = await fetchJSON(`${API_BASE}/skill/${encodeURIComponent(args.slug)}`);
@@ -432,6 +595,12 @@ async function handleGetSkill(args) {
432
595
  async function handleInstallSkill(args) {
433
596
  const params = new URLSearchParams({ agent: args.agent });
434
597
  const result = await fetchJSON(`${API_BASE}/install/${encodeURIComponent(args.slug)}?${params.toString()}`, { "X-Agent-Key": AGENT_KEY });
598
+ // Fire-and-forget: save this slug to installed_skills memory
599
+ const memories = await loadAgentMemory().catch(() => []);
600
+ const existing = extractInstalledSlugs(memories);
601
+ if (!existing.includes(args.slug)) {
602
+ saveMemoryAsync("installed_skills", [...existing, args.slug], "install");
603
+ }
435
604
  return JSON.stringify(result, null, 2);
436
605
  }
437
606
  async function handleListCategories() {
@@ -439,9 +608,27 @@ async function handleListCategories() {
439
608
  return JSON.stringify(result, null, 2);
440
609
  }
441
610
  async function handleRecommendSkills(args) {
442
- const params = new URLSearchParams({ context: args.context });
443
- if (args.installed)
444
- params.set("installed", args.installed);
611
+ // Auto-load agent memory for personalization
612
+ const memories = await loadAgentMemory();
613
+ const installedSlugs = extractInstalledSlugs(memories);
614
+ const recentSearches = extractRecentSearches(memories);
615
+ // Merge installed skills from memory with any explicitly provided
616
+ const allInstalled = new Set(installedSlugs);
617
+ if (args.installed) {
618
+ for (const s of args.installed.split(",").map((s) => s.trim()).filter(Boolean)) {
619
+ allInstalled.add(s);
620
+ }
621
+ }
622
+ // Enrich context with relevant search history keywords
623
+ let enrichedContext = args.context;
624
+ if (recentSearches.length > 0) {
625
+ const recentKeywords = recentSearches.slice(-5).join(", ");
626
+ enrichedContext = `${args.context} (recent interests: ${recentKeywords})`;
627
+ }
628
+ const params = new URLSearchParams({ context: enrichedContext });
629
+ if (allInstalled.size > 0) {
630
+ params.set("installed", Array.from(allInstalled).join(","));
631
+ }
445
632
  const result = await fetchJSON(`${API_BASE}/recommend?${params.toString()}`, { "X-Agent-Key": AGENT_KEY });
446
633
  return JSON.stringify(result, null, 2);
447
634
  }
@@ -469,13 +656,64 @@ async function handleReportSkillUsage(args) {
469
656
  const body = {
470
657
  slug: args.slug,
471
658
  status: args.status,
659
+ agent: "claude-code",
660
+ agent_key: AGENT_KEY,
472
661
  };
473
662
  if (args.error_message) {
474
663
  body.error_message = args.error_message;
475
664
  }
476
- const result = await postJSON(`${API_BASE}/report`, body);
665
+ const result = (await postJSON(`${API_BASE}/report`, body));
666
+ if (result.proof) {
667
+ const lines = [
668
+ "Skill usage reported successfully.",
669
+ "",
670
+ "Execution Proof:",
671
+ ` Proof ID: ${result.proof.proof_id}`,
672
+ ` Verify: ${result.proof.verify_url}`,
673
+ ` ${result.proof.shareable_text}`,
674
+ ];
675
+ return lines.join("\n");
676
+ }
477
677
  return JSON.stringify(result, null, 2);
478
678
  }
679
+ async function handleListMyProofs() {
680
+ const params = new URLSearchParams({ agent_key: AGENT_KEY });
681
+ const result = (await fetchJSON(`${API_BASE}/proofs?${params.toString()}`));
682
+ if (result.error) {
683
+ return `Failed to list proofs: ${result.error}`;
684
+ }
685
+ const proofs = result.proofs ?? [];
686
+ if (proofs.length === 0) {
687
+ return "No execution proofs yet. Report successful skill usage to earn proofs.";
688
+ }
689
+ const lines = [
690
+ `Execution Proofs (${proofs.length} total):`,
691
+ "",
692
+ ];
693
+ proofs.forEach((p, i) => {
694
+ lines.push(`${i + 1}. ${p.skill_name ?? p.skill_slug} [${p.proof_id}]`);
695
+ lines.push(` Skill: ${p.skill_slug}`);
696
+ lines.push(` Verify: ${p.verify_url}`);
697
+ lines.push(` Date: ${p.created_at ?? "unknown"}`);
698
+ lines.push("");
699
+ });
700
+ return lines.join("\n");
701
+ }
702
+ async function handleVerifyProof(args) {
703
+ const result = (await fetchJSON(`${API_BASE}/verify/${encodeURIComponent(args.proof_id)}`));
704
+ if (result.error) {
705
+ return `Verification failed: ${result.error}`;
706
+ }
707
+ if (!result.valid) {
708
+ return `Proof ${args.proof_id} is NOT valid.`;
709
+ }
710
+ return [
711
+ `Proof ${result.proof_id} is VALID.`,
712
+ ` Skill: ${result.skill}`,
713
+ ` Verified at: ${result.verified_at}`,
714
+ ` Platform: ${result.platform}`,
715
+ ].join("\n");
716
+ }
479
717
  async function handleSaveMemory(args) {
480
718
  const body = {
481
719
  agent_key: AGENT_KEY,
@@ -554,6 +792,83 @@ async function handleInstallPack(args) {
554
792
  });
555
793
  return lines.join("\n");
556
794
  }
795
+ async function handleSmartSearch(args) {
796
+ // Load all agent memory (installed skills, recent searches, preferences)
797
+ const memories = await loadAgentMemory();
798
+ const installedSlugs = extractInstalledSlugs(memories);
799
+ const recentSearches = extractRecentSearches(memories);
800
+ const params = new URLSearchParams({ q: args.query });
801
+ if (args.limit)
802
+ params.set("limit", String(args.limit));
803
+ const result = await fetchJSON(`${API_BASE}/search?${params.toString()}`, {
804
+ "X-Agent-Key": AGENT_KEY,
805
+ });
806
+ // Fire-and-forget: record this search query
807
+ recordSearchQuery(args.query, recentSearches);
808
+ // Filter out already-installed skills
809
+ const parsed = result;
810
+ const excludedCount = { value: 0 };
811
+ if (parsed.results && installedSlugs.length > 0) {
812
+ const installedSet = new Set(installedSlugs);
813
+ const originalLength = parsed.results.length;
814
+ parsed.results = parsed.results.filter((r) => !r.slug || !installedSet.has(r.slug));
815
+ excludedCount.value = originalLength - parsed.results.length;
816
+ }
817
+ // Add metadata about personalization
818
+ const output = { ...parsed };
819
+ output._personalization = {
820
+ installed_skills_excluded: excludedCount.value,
821
+ installed_skills_count: installedSlugs.length,
822
+ recent_searches: recentSearches.slice(-5),
823
+ };
824
+ return JSON.stringify(output, null, 2);
825
+ }
826
+ async function handleFlagSkill(args) {
827
+ const body = {
828
+ reason: args.reason,
829
+ };
830
+ if (args.details) {
831
+ body.details = args.details;
832
+ }
833
+ const url = `https://loaditout.ai/api/skills/${encodeURIComponent(args.slug)}/flag`;
834
+ const result = (await postJSON(url, body, { "X-Agent-Key": AGENT_KEY }));
835
+ if (result.error) {
836
+ return `Flag failed: ${result.error}`;
837
+ }
838
+ return `Skill "${args.slug}" has been flagged for "${args.reason}". Thank you for helping keep the registry safe.`;
839
+ }
840
+ async function handleValidateAction(args) {
841
+ const body = {
842
+ slug: args.slug,
843
+ action: args.action,
844
+ agent_key: AGENT_KEY,
845
+ };
846
+ if (args.parameters) {
847
+ body.parameters = args.parameters;
848
+ }
849
+ const result = (await postJSON(`${API_BASE}/validate`, body));
850
+ if (result.error) {
851
+ return `Validation failed: ${result.error}`;
852
+ }
853
+ const lines = [
854
+ `Validation result for ${args.slug} / ${args.action}:`,
855
+ ` Safe to proceed: ${result.valid ? "YES" : "NO"}`,
856
+ ` Risk level: ${result.risk_level ?? "unknown"}`,
857
+ ` Security grade: ${result.security_grade ?? "unknown"}`,
858
+ ` Skill verified: ${result.skill_verified ? "yes" : "no"}`,
859
+ ];
860
+ if (result.last_updated_days_ago !== undefined) {
861
+ lines.push(` Last updated: ${result.last_updated_days_ago} days ago`);
862
+ }
863
+ const warnings = result.warnings ?? [];
864
+ if (warnings.length > 0) {
865
+ lines.push(` Warnings:`);
866
+ for (const w of warnings) {
867
+ lines.push(` - ${w}`);
868
+ }
869
+ }
870
+ return lines.join("\n");
871
+ }
557
872
  function makeResponse(id, result) {
558
873
  return { jsonrpc: "2.0", id, result };
559
874
  }
@@ -617,6 +932,12 @@ async function handleRequest(request) {
617
932
  case "report_skill_usage":
618
933
  resultText = await handleReportSkillUsage(toolArgs);
619
934
  break;
935
+ case "list_my_proofs":
936
+ resultText = await handleListMyProofs();
937
+ break;
938
+ case "verify_proof":
939
+ resultText = await handleVerifyProof(toolArgs);
940
+ break;
620
941
  case "save_memory":
621
942
  resultText = await handleSaveMemory(toolArgs);
622
943
  break;
@@ -638,6 +959,15 @@ async function handleRequest(request) {
638
959
  case "install_pack":
639
960
  resultText = await handleInstallPack(toolArgs);
640
961
  break;
962
+ case "validate_action":
963
+ resultText = await handleValidateAction(toolArgs);
964
+ break;
965
+ case "flag_skill":
966
+ resultText = await handleFlagSkill(toolArgs);
967
+ break;
968
+ case "smart_search":
969
+ resultText = await handleSmartSearch(toolArgs);
970
+ break;
641
971
  default:
642
972
  send(makeError(id, -32601, `Unknown tool: ${toolName}`));
643
973
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loaditout-mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for discovering and installing AI agent skills from Loaditout",
5
5
  "bin": {
6
6
  "loaditout-mcp": "./dist/index.js"