@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.
- package/README.md +24 -0
- package/index.mjs +43 -14
- package/package.json +3 -2
- 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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
+
}
|