@aistoragedepot/mcp 0.1.0 → 0.3.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 (4) hide show
  1. package/README.md +24 -0
  2. package/index.mjs +43 -14
  3. package/package.json +3 -2
  4. package/pull.mjs +90 -0
package/README.md CHANGED
@@ -16,6 +16,9 @@ Your stored content shows up where you actually work:
16
16
 
17
17
  ## Setup
18
18
 
19
+ > **👉 Easiest path:** step-by-step instructions for each app (config-file locations and all)
20
+ > are at **<https://www.aistoragedepot.com/connect>**. The manual version:
21
+
19
22
  1. In AIStorageDepot (a **PLUS** plan is required for API tokens), go to **Settings → API
20
23
  tokens** and create a token. Copy it.
21
24
  2. Add the server to your MCP client's config. Example (Claude Desktop /
@@ -44,6 +47,27 @@ Your stored content shows up where you actually work:
44
47
  | --- | --- | --- | --- |
45
48
  | `AISD_TOKEN` | yes | — | Token from Settings → API tokens. Has full access to your account — keep it secret. |
46
49
  | `AISD_BASE_URL` | no | `https://www.aistoragedepot.com` | Point at a self-hosted / dev instance if needed. |
50
+ | `AISD_WORKSPACES` | no | your personal library | Which workspaces expose **prompts** (slash-commands). Comma-separated workspace names (e.g. `My Library,Engineering`), or `all`. Keeps the slash menu short; **search and resources still cover every workspace** regardless. |
51
+
52
+ ## Slash-commands (`pull`)
53
+
54
+ Not every client turns MCP prompts into `/` slash-commands (some only use the tools). To get
55
+ your prompts as **native** slash-commands that always work, sync them to command files:
56
+
57
+ ```bash
58
+ npx -y @aistoragedepot/mcp pull --token=aisd_your_token
59
+ ```
60
+
61
+ This writes `~/.claude/commands/aistoragedepot/*.md` — start a new chat and type
62
+ **`/aistoragedepot`** to see them. Re-run any time; it only clears and rewrites its own
63
+ namespaced folder.
64
+
65
+ | Option | Default | |
66
+ | --- | --- | --- |
67
+ | `--token=…` | `AISD_TOKEN` env | Your API token. |
68
+ | `--workspaces="A,B"` | your personal library | Which workspaces to pull (names, or `all`). |
69
+ | `--project` | off | Write to `./.claude/commands/…` (this repo) instead of your home dir. |
70
+ | `--dir=<path>` | `~/.claude/commands/aistoragedepot` | Write somewhere else entirely. |
47
71
 
48
72
  ## Notes
49
73
 
package/index.mjs CHANGED
@@ -22,6 +22,14 @@ import {
22
22
  } from "@modelcontextprotocol/sdk/types.js";
23
23
 
24
24
  const BASE_URL = (process.env.AISD_BASE_URL || "https://www.aistoragedepot.com").replace(/\/+$/, "");
25
+
26
+ // CLI mode: `aistoragedepot-mcp pull` syncs prompts to native slash-command files, then exits.
27
+ if (process.argv[2] === "pull") {
28
+ const { pull } = await import("./pull.mjs");
29
+ await pull({ BASE_URL, argv: process.argv.slice(3) });
30
+ process.exit(0);
31
+ }
32
+
25
33
  const TOKEN = process.env.AISD_TOKEN;
26
34
 
27
35
  if (!TOKEN) {
@@ -43,15 +51,13 @@ let cache = { at: 0, workspaces: [], items: [] };
43
51
  async function snapshot() {
44
52
  if (cache.items.length && Date.now() - cache.at < 30_000) return cache;
45
53
  const workspaces = await api("/api/workspaces");
46
- const items = [];
47
- for (const ws of workspaces) {
48
- try {
49
- const lib = await api(`/api/library?workspace=${encodeURIComponent(ws.id)}`);
50
- for (const it of lib.items || []) items.push(it);
51
- } catch {
52
- // a workspace we can't read (or that errored) skip it, keep the rest.
53
- }
54
- }
54
+ // Fetch every workspace's library in parallel — the sequential loop was the slow part.
55
+ const libs = await Promise.all(
56
+ workspaces.map((ws) =>
57
+ api(`/api/library?workspace=${encodeURIComponent(ws.id)}`).catch(() => ({ items: [] })),
58
+ ),
59
+ );
60
+ const items = libs.flatMap((lib) => lib.items || []);
55
61
  cache = { at: Date.now(), workspaces, items };
56
62
  return cache;
57
63
  }
@@ -76,6 +82,31 @@ function promptIndex(items) {
76
82
  return byName;
77
83
  }
78
84
 
85
+ // Which workspaces' prompts appear as slash-commands. Default: your personal library only —
86
+ // keeps the slash menu short + fast (clients bury/limit long prompt lists). Override with
87
+ // AISD_WORKSPACES="Name1,Name2" (comma-separated workspace names) or AISD_WORKSPACES="all".
88
+ const PROMPT_WS = (process.env.AISD_WORKSPACES || "")
89
+ .split(",")
90
+ .map((s) => s.trim().toLowerCase())
91
+ .filter(Boolean);
92
+
93
+ function promptScopeIds(workspaces) {
94
+ const all = () => new Set(workspaces.map((w) => w.id));
95
+ if (PROMPT_WS.includes("all") || PROMPT_WS.includes("*")) return all();
96
+ if (PROMPT_WS.length) {
97
+ const named = workspaces.filter((w) => PROMPT_WS.includes((w.name || "").toLowerCase()));
98
+ if (named.length) return new Set(named.map((w) => w.id));
99
+ }
100
+ const personal = workspaces.filter((w) => w.type === "PERSONAL");
101
+ return personal.length ? new Set(personal.map((w) => w.id)) : all();
102
+ }
103
+
104
+ // promptIndex, but only over items in the in-scope workspaces.
105
+ function scopedPromptIndex(cache) {
106
+ const ids = promptScopeIds(cache.workspaces);
107
+ return promptIndex(cache.items.filter((it) => ids.has(it.workspaceId)));
108
+ }
109
+
79
110
  // MCP argument names should be simple identifiers; map them back to the original
80
111
  // "[placeholder]" text so we can substitute into the body.
81
112
  function argMap(placeholders) {
@@ -100,7 +131,7 @@ function fillBody(body, placeholders, args) {
100
131
  }
101
132
 
102
133
  const server = new Server(
103
- { name: "aistoragedepot", version: "0.1.0" },
134
+ { name: "aistoragedepot", version: "0.3.0" },
104
135
  { capabilities: { resources: {}, prompts: {}, tools: {} } },
105
136
  );
106
137
 
@@ -128,9 +159,8 @@ server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
128
159
 
129
160
  // ---- Prompts: prompt-format items, [fields] → arguments ----
130
161
  server.setRequestHandler(ListPromptsRequestSchema, async () => {
131
- const { items } = await snapshot();
132
162
  const prompts = [];
133
- for (const [name, it] of promptIndex(items)) {
163
+ for (const [name, it] of scopedPromptIndex(await snapshot())) {
134
164
  const am = argMap(it.placeholders);
135
165
  prompts.push({
136
166
  name,
@@ -146,8 +176,7 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
146
176
  });
147
177
 
148
178
  server.setRequestHandler(GetPromptRequestSchema, async (req) => {
149
- const { items } = await snapshot();
150
- const it = promptIndex(items).get(req.params.name);
179
+ const it = scopedPromptIndex(await snapshot()).get(req.params.name);
151
180
  if (!it) throw new Error(`Unknown prompt: ${req.params.name}`);
152
181
  const text = fillBody(it.body, it.placeholders, req.params.arguments || {});
153
182
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aistoragedepot/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Model Context Protocol server for your AIStorageDepot library — exposes your prompts, rules, docs, and skills to MCP-aware AI clients (Claude, Cursor, Cline, …).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,8 @@
9
9
  "files": [
10
10
  "index.mjs",
11
11
  "README.md",
12
- "LICENSE"
12
+ "LICENSE",
13
+ "pull.mjs"
13
14
  ],
14
15
  "engines": {
15
16
  "node": ">=18"
package/pull.mjs ADDED
@@ -0,0 +1,90 @@
1
+ // `aistoragedepot-mcp pull` — sync your library into native slash-command files.
2
+ //
3
+ // MCP prompts-as-slash-commands are supported inconsistently across clients, so this writes
4
+ // real command files that any Claude Code client picks up: ~/.claude/commands/aistoragedepot/*.md
5
+ // (a /aistoragedepot:<name> command each). Defaults to the **skills** in your personal library
6
+ // (self-contained instructions that run cleanly as commands); widen with --workspaces / include
7
+ // prompts with --types. Re-run any time — it clears and rewrites only its own namespaced folder.
8
+ import { mkdir, writeFile, rm } from "node:fs/promises";
9
+ import { homedir } from "node:os";
10
+ import { join, resolve } from "node:path";
11
+
12
+ const slug = (s) =>
13
+ String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "item";
14
+ const typeOf = (it) => (it.type?.slug || it.type?.name || "").toLowerCase();
15
+
16
+ function parseArgs(argv) {
17
+ const o = {};
18
+ for (const a of argv) {
19
+ const m = a.match(/^--([^=]+)(?:=(.*))?$/);
20
+ if (m) o[m[1]] = m[2] ?? true;
21
+ }
22
+ return o;
23
+ }
24
+
25
+ export async function pull({ BASE_URL, argv }) {
26
+ const o = parseArgs(argv);
27
+ const token = o.token || process.env.AISD_TOKEN;
28
+ if (!token) {
29
+ console.error(
30
+ "A token is required. Pass --token=aisd_… (create one in AIStorageDepot → Settings → API tokens), or set AISD_TOKEN.",
31
+ );
32
+ process.exit(1);
33
+ }
34
+ const api = async (p) => {
35
+ const r = await fetch(BASE_URL + p, { headers: { Authorization: `Bearer ${token}`, Accept: "application/json" } });
36
+ if (r.status === 401) throw new Error("Token rejected (401) — check it in Settings → API tokens.");
37
+ if (!r.ok) throw new Error(`${p} → HTTP ${r.status}`);
38
+ return r.json();
39
+ };
40
+
41
+ // Which item types to pull. Default: skills only. --types=skill,prompt | all.
42
+ const TYPES = String(o.types || "skill").toLowerCase().split(",").map((s) => s.trim()).filter(Boolean);
43
+ const wantType = (it) => {
44
+ const t = typeOf(it);
45
+ const isSkill = t === "skill";
46
+ const isPrompt = it.type?.format === "prompt" || t === "prompt";
47
+ if (TYPES.includes("all")) return isSkill || isPrompt;
48
+ return (TYPES.includes("skill") && isSkill) || (TYPES.includes("prompt") && isPrompt) || TYPES.includes(t);
49
+ };
50
+
51
+ const workspaces = await api("/api/workspaces");
52
+ // Scope: default every workspace; --workspaces="A,B" | "personal" | "all".
53
+ const names = String(o.workspaces || "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
54
+ let scope;
55
+ if (names.includes("all") || names.includes("*")) scope = workspaces;
56
+ else if (names.length) scope = workspaces.filter((w) => names.includes((w.name || "").toLowerCase()));
57
+ else scope = workspaces.filter((w) => w.type === "PERSONAL"); // default: just your personal library
58
+
59
+ const libs = await Promise.all(
60
+ scope.map((w) => api(`/api/library?workspace=${encodeURIComponent(w.id)}`).catch(() => ({ items: [] }))),
61
+ );
62
+ const items = libs.flatMap((l) => (l.items || []).filter(wantType));
63
+
64
+ const dir = o.dir
65
+ ? resolve(String(o.dir))
66
+ : o.project
67
+ ? resolve(".claude", "commands", "aistoragedepot")
68
+ : join(homedir(), ".claude", "commands", "aistoragedepot");
69
+ await rm(dir, { recursive: true, force: true }); // clean re-sync of our namespaced folder only
70
+ await mkdir(dir, { recursive: true });
71
+
72
+ const used = new Set();
73
+ let n = 0;
74
+ for (const it of items) {
75
+ let name = slug(it.title);
76
+ while (used.has(name)) name = `${name}-${it.id.slice(-4)}`;
77
+ used.add(name);
78
+ const body = it.body || "";
79
+ // Skills already carry their own YAML frontmatter (name/description) — keep it as-is; for anything
80
+ // without frontmatter (e.g. prompts), add a description so it shows nicely in the / menu.
81
+ const content = /^---\r?\n/.test(body)
82
+ ? body.endsWith("\n") ? body : `${body}\n`
83
+ : `---\ndescription: "${`${it.title} (AIStorageDepot)`.replace(/"/g, "'").replace(/\s+/g, " ").trim()}"\n---\n\n${body}\n`;
84
+ await writeFile(join(dir, `${name}.md`), content, "utf8");
85
+ n++;
86
+ }
87
+
88
+ console.log(`✓ Synced ${n} item${n === 1 ? "" : "s"} (${TYPES.join(", ")}) → ${dir}`);
89
+ console.log(` Start a new chat and type /aistoragedepot to use them.`);
90
+ }