@aistoragedepot/mcp 0.5.0 → 0.5.3

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 +1 -1
  2. package/index.mjs +39 -4
  3. package/package.json +2 -1
  4. package/pull.mjs +23 -11
package/README.md CHANGED
@@ -56,7 +56,7 @@ your library as **native** slash-commands, sync it to command files — for ever
56
56
  use, not just Claude:
57
57
 
58
58
  ```bash
59
- npx -y @aistoragedepot/mcp pull --token=aisd_your_token --to=all
59
+ npx -y -p @aistoragedepot/mcp aistoragedepot-mcp pull --token=aisd_your_token --to=all
60
60
  ```
61
61
 
62
62
  | Target | Writes to | Commands appear as |
package/index.mjs CHANGED
@@ -23,7 +23,7 @@ import {
23
23
 
24
24
  const BASE_URL = (process.env.AISD_BASE_URL || "https://www.aistoragedepot.com").replace(/\/+$/, "");
25
25
 
26
- // CLI mode: `aistoragedepot-mcp pull` syncs the library to native slash-command files, then
26
+ // CLI mode: `npx -y @aistoragedepot/mcp pull` syncs the library to native slash-command files, then
27
27
  // exits. The brief settle delay lets undici finish closing its sockets (Connection: close)
28
28
  // before process.exit — a hard exit mid-teardown trips a libuv assert on Windows (Node 24)
29
29
  // that prints a scary-but-harmless message after the sync succeeds.
@@ -52,7 +52,12 @@ async function api(path) {
52
52
  headers: { Authorization: `Bearer ${TOKEN}`, Accept: "application/json" },
53
53
  });
54
54
  if (res.status === 401) throw new Error("AIStorageDepot rejected the token (401) — check AISD_TOKEN.");
55
- if (!res.ok) throw new Error(`AIStorageDepot ${path} → HTTP ${res.status}`);
55
+ if (!res.ok) {
56
+ // Surface the server's own message when it sends one (e.g. the free-plan
57
+ // monthly pull limit returns 402 with an upgrade note) instead of a bare code.
58
+ const detail = await res.json().then((d) => d?.error).catch(() => null);
59
+ throw new Error(detail || `AIStorageDepot ${path} → HTTP ${res.status}`);
60
+ }
56
61
  return res.json();
57
62
  }
58
63
 
@@ -140,6 +145,34 @@ function fillBody(body, placeholders, args) {
140
145
  return out;
141
146
  }
142
147
 
148
+ // Which MCP arg-names are required (their [placeholder] was marked required by the author).
149
+ function requiredArgNames(placeholders, requiredFields = []) {
150
+ const req = new Set(requiredFields);
151
+ const out = new Set();
152
+ for (const [argName, original] of argMap(placeholders)) if (req.has(original)) out.add(argName);
153
+ return out;
154
+ }
155
+
156
+ // A "stop and ask first" instruction for any required field missing from the args — prepended to the
157
+ // prompt so even clients that ignore `required` don't run half-blank. "" when nothing's missing.
158
+ function missingRequiredPreamble(placeholders, requiredFields = [], args = {}) {
159
+ const req = new Set(requiredFields);
160
+ const missing = [];
161
+ for (const [argName, original] of argMap(placeholders)) {
162
+ if (!req.has(original)) continue;
163
+ const v = args?.[argName];
164
+ if (v == null || v === "") missing.push(original);
165
+ }
166
+ if (!missing.length) return "";
167
+ const list = missing.map((f) => `[${f}]`).join(", ");
168
+ const one = missing.length === 1;
169
+ return (
170
+ `⚠ Before doing anything else, ask me for a value for ${one ? "this field" : "these fields"}: ${list}. ` +
171
+ `Do not explore, run, generate, or take any action until I answer. Then substitute my ${one ? "answer" : "answers"} ` +
172
+ `for ${one ? "that field" : "those fields"} and carry out the prompt below.\n\n`
173
+ );
174
+ }
175
+
143
176
  const server = new Server(
144
177
  { name: "aistoragedepot", version: "0.5.0" },
145
178
  { capabilities: { resources: {}, prompts: {}, tools: {} } },
@@ -172,13 +205,14 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
172
205
  const prompts = [];
173
206
  for (const [name, it] of scopedPromptIndex(await snapshot())) {
174
207
  const am = argMap(it.placeholders);
208
+ const req = requiredArgNames(it.placeholders, it.requiredFields);
175
209
  prompts.push({
176
210
  name,
177
211
  description: `${it.type?.name || "Prompt"}: “${it.title}” (AIStorageDepot)`,
178
212
  arguments: [...am.keys()].map((a) => ({
179
213
  name: a,
180
214
  description: `Value for [${am.get(a)}]`,
181
- required: false,
215
+ required: req.has(a),
182
216
  })),
183
217
  });
184
218
  }
@@ -188,7 +222,8 @@ server.setRequestHandler(ListPromptsRequestSchema, async () => {
188
222
  server.setRequestHandler(GetPromptRequestSchema, async (req) => {
189
223
  const it = scopedPromptIndex(await snapshot()).get(req.params.name);
190
224
  if (!it) throw new Error(`Unknown prompt: ${req.params.name}`);
191
- const text = fillBody(it.body, it.placeholders, req.params.arguments || {});
225
+ const args = req.params.arguments || {};
226
+ const text = missingRequiredPreamble(it.placeholders, it.requiredFields, args) + fillBody(it.body, it.placeholders, args);
192
227
  return {
193
228
  description: it.title,
194
229
  messages: [{ role: "user", content: { type: "text", text } }],
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@aistoragedepot/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.3",
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": {
7
+ "mcp": "index.mjs",
7
8
  "aistoragedepot-mcp": "index.mjs"
8
9
  },
9
10
  "files": [
package/pull.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // `aistoragedepot-mcp pull` — sync your library into native slash-command files for the
1
+ // `npx @aistoragedepot/mcp pull` — sync your library into native slash-command files for the
2
2
  // AI tools you use.
3
3
  //
4
4
  // Every major client now reads commands from a folder of markdown files; only the folder
@@ -52,14 +52,22 @@ function fmFile(fm, body) {
52
52
  return ["---", ...Object.entries(fm).map(([k, v]) => `${k}: "${yq(v)}"`), "---", "", String(body).trim(), ""].join("\n");
53
53
  }
54
54
 
55
+ // Required fields the author marked (intersected with the fields actually in this body).
56
+ const reqOf = (it, fields) => (it.requiredFields || []).filter((f) => fields.includes(f));
57
+ // A forceful "these are required — ask first" clause, or the gentle version when none are required.
58
+ const askClause = (required) =>
59
+ required.length
60
+ ? ` ${required.length === 1 ? "This field is" : "These fields are"} REQUIRED: ${required.map((f) => `[${f}]`).join(", ")} — if a value wasn't provided, STOP and ask me first; don't explore, run, or generate anything until I answer.`
61
+ : ` Ask me for any value that's missing.`;
62
+
55
63
  // Tail for tools that substitute $ARGUMENTS (claude, codex).
56
- const argsTail = (fields) =>
64
+ const argsTail = (fields, required = []) =>
57
65
  fields.length
58
- ? `\n\nInput: $ARGUMENTS\n\nFill the bracketed ${fields.length === 1 ? "field" : "fields"} in the prompt above from the input ask me for any value that's missing — then carry out the prompt.`
66
+ ? `\n\nInput: $ARGUMENTS\n\nFill the bracketed ${fields.length === 1 ? "field" : "fields"} in the prompt above from the input.${askClause(required)} Then carry out the prompt.`
59
67
  : "\n\n$ARGUMENTS";
60
68
  // Tail for tools that just pass the typed text along with the inserted prompt.
61
- const fillTail = (fields) =>
62
- `\n\nFill the bracketed ${fields.length === 1 ? "field" : "fields"} (${fields.map((f) => `[${f}]`).join(", ")}) from anything I typed along with this command ask me for any value that's missing — then carry out the prompt.`;
69
+ const fillTail = (fields, required = []) =>
70
+ `\n\nFill the bracketed ${fields.length === 1 ? "field" : "fields"} (${fields.map((f) => `[${f}]`).join(", ")}) from anything I typed along with this command.${askClause(required)} Then carry out the prompt.`;
63
71
 
64
72
  // ── per-target renderers ─────────────────────────────────────────────────────
65
73
 
@@ -72,7 +80,7 @@ function renderClaude(it) {
72
80
  const fields = fieldsOf(content);
73
81
  const fm = { description: `Prompt — ${it.title} (AIStorageDepot)` };
74
82
  if (fields.length) fm["argument-hint"] = fields.map((f) => `[${f}]`).join(" ");
75
- return fmFile(fm, content.trim() + argsTail(fields));
83
+ return fmFile(fm, content.trim() + argsTail(fields, reqOf(it, fields)));
76
84
  }
77
85
  if (/^---\r?\n/.test(body)) return body.endsWith("\n") ? body : `${body}\n`;
78
86
  return fmFile({ description: `${it.title} (AIStorageDepot)` }, body);
@@ -85,7 +93,7 @@ function renderCodex(it) {
85
93
  const fields = isPromptItem(it) ? fieldsOf(content) : [];
86
94
  const fm = { description: descFor(it, meta) };
87
95
  if (isPromptItem(it) && fields.length) fm["argument-hint"] = fields.map((f) => `[${f}]`).join(" ");
88
- return fmFile(fm, content.trim() + (isPromptItem(it) ? argsTail(fields) : ""));
96
+ return fmFile(fm, content.trim() + (isPromptItem(it) ? argsTail(fields, reqOf(it, fields)) : ""));
89
97
  }
90
98
 
91
99
  // VS Code prompt files: description + argument-hint frontmatter; typed extras arrive as
@@ -95,21 +103,21 @@ function renderVscode(it) {
95
103
  const fields = isPromptItem(it) ? fieldsOf(content) : [];
96
104
  const fm = { description: descFor(it, meta) };
97
105
  if (fields.length) fm["argument-hint"] = fields.map((f) => `[${f}]`).join(" ");
98
- return fmFile(fm, content.trim() + (fields.length ? fillTail(fields) : ""));
106
+ return fmFile(fm, content.trim() + (fields.length ? fillTail(fields, reqOf(it, fields)) : ""));
99
107
  }
100
108
 
101
109
  // Windsurf workflows: description frontmatter; 12k char cap per file (enforced by caller).
102
110
  function renderWindsurf(it) {
103
111
  const { meta, content } = splitFrontmatter(it.body || "");
104
112
  const fields = isPromptItem(it) ? fieldsOf(content) : [];
105
- return fmFile({ description: descFor(it, meta) }, content.trim() + (fields.length ? fillTail(fields) : ""));
113
+ return fmFile({ description: descFor(it, meta) }, content.trim() + (fields.length ? fillTail(fields, reqOf(it, fields)) : ""));
106
114
  }
107
115
 
108
116
  // Cursor commands: plain markdown — no frontmatter dialect; the body IS the prompt.
109
117
  function renderCursor(it) {
110
118
  const { content } = splitFrontmatter(it.body || "");
111
119
  const fields = isPromptItem(it) ? fieldsOf(content) : [];
112
- return content.trim() + (fields.length ? fillTail(fields) : "") + "\n";
120
+ return content.trim() + (fields.length ? fillTail(fields, reqOf(it, fields)) : "") + "\n";
113
121
  }
114
122
 
115
123
  // ── targets ──────────────────────────────────────────────────────────────────
@@ -196,7 +204,11 @@ export async function pull({ BASE_URL, argv }) {
196
204
  headers: { Authorization: `Bearer ${token}`, Accept: "application/json", Connection: "close" },
197
205
  });
198
206
  if (r.status === 401) throw new Error("Token rejected (401) — check it in Settings → API tokens.");
199
- if (!r.ok) throw new Error(`${p} → HTTP ${r.status}`);
207
+ if (!r.ok) {
208
+ // Prefer the server's message (e.g. the free-plan pull limit's 402 upgrade note).
209
+ const detail = await r.json().then((d) => d?.error).catch(() => null);
210
+ throw new Error(detail || `${p} → HTTP ${r.status}`);
211
+ }
200
212
  return r.json();
201
213
  };
202
214