@aistoragedepot/mcp 0.1.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/LICENSE +21 -0
- package/README.md +59 -0
- package/index.mjs +215 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AIStorageDepot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# @aistoragedepot/mcp
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes **your
|
|
4
|
+
[AIStorageDepot](https://www.aistoragedepot.com) library** to any MCP-aware AI client —
|
|
5
|
+
Claude Desktop, Cursor, Cline, Windsurf, and others.
|
|
6
|
+
|
|
7
|
+
Your stored content shows up where you actually work:
|
|
8
|
+
|
|
9
|
+
- **Prompts** → your prompt-format items become MCP prompts (slash-commands), with each
|
|
10
|
+
`[field]` surfaced as a prompt argument you fill in.
|
|
11
|
+
- **Resources** → every item (rules, docs, skills, MCP configs, prompts) is readable at
|
|
12
|
+
`aisd://item/<id>`, so an agent can pull "my coding rules" or "my project guidelines"
|
|
13
|
+
into context on demand.
|
|
14
|
+
- **Tools** → `search_library(query)` and `get_item(id)` let an agent find and fetch
|
|
15
|
+
anything in your library programmatically.
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
1. In AIStorageDepot (a **PLUS** plan is required for API tokens), go to **Settings → API
|
|
20
|
+
tokens** and create a token. Copy it.
|
|
21
|
+
2. Add the server to your MCP client's config. Example (Claude Desktop /
|
|
22
|
+
`claude_desktop_config.json`, Cursor `~/.cursor/mcp.json`, etc.):
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"aistoragedepot": {
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["-y", "@aistoragedepot/mcp"],
|
|
30
|
+
"env": {
|
|
31
|
+
"AISD_TOKEN": "aisd_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
3. Restart the client. Your prompts appear as slash-commands; your items are available as
|
|
39
|
+
resources; and the `search_library` / `get_item` tools are ready.
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
|
|
43
|
+
| Env var | Required | Default | Notes |
|
|
44
|
+
| --- | --- | --- | --- |
|
|
45
|
+
| `AISD_TOKEN` | yes | — | Token from Settings → API tokens. Has full access to your account — keep it secret. |
|
|
46
|
+
| `AISD_BASE_URL` | no | `https://www.aistoragedepot.com` | Point at a self-hosted / dev instance if needed. |
|
|
47
|
+
|
|
48
|
+
## Notes
|
|
49
|
+
|
|
50
|
+
- Read-only in v0.1 (it surfaces and fetches your content; it doesn't write back).
|
|
51
|
+
- The library snapshot is cached for ~30s, so a burst of calls hits the API once.
|
|
52
|
+
- Revoke a token any time in Settings → API tokens; the server loses access immediately.
|
|
53
|
+
|
|
54
|
+
## Local development
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install
|
|
58
|
+
AISD_TOKEN=<token> AISD_BASE_URL=http://localhost:3100 npm run test:client
|
|
59
|
+
```
|
package/index.mjs
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// AIStorageDepot MCP server.
|
|
3
|
+
//
|
|
4
|
+
// Exposes the signed-in user's library to any MCP-aware AI client (Claude Desktop,
|
|
5
|
+
// Cursor, Cline, Windsurf, …) over stdio:
|
|
6
|
+
// • Prompts — prompt-format items, with their [fields] surfaced as prompt arguments.
|
|
7
|
+
// • Resources — every item (rules, docs, skills, configs, prompts) readable by URI.
|
|
8
|
+
// • Tools — search_library(query) and get_item(id).
|
|
9
|
+
//
|
|
10
|
+
// Auth + target are configured via env:
|
|
11
|
+
// AISD_TOKEN (required) a token from AIStorageDepot → Settings → API tokens.
|
|
12
|
+
// AISD_BASE_URL (optional) defaults to https://www.aistoragedepot.com.
|
|
13
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import {
|
|
16
|
+
ListResourcesRequestSchema,
|
|
17
|
+
ReadResourceRequestSchema,
|
|
18
|
+
ListPromptsRequestSchema,
|
|
19
|
+
GetPromptRequestSchema,
|
|
20
|
+
ListToolsRequestSchema,
|
|
21
|
+
CallToolRequestSchema,
|
|
22
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
|
|
24
|
+
const BASE_URL = (process.env.AISD_BASE_URL || "https://www.aistoragedepot.com").replace(/\/+$/, "");
|
|
25
|
+
const TOKEN = process.env.AISD_TOKEN;
|
|
26
|
+
|
|
27
|
+
if (!TOKEN) {
|
|
28
|
+
console.error("AISD_TOKEN is required. Create one in AIStorageDepot → Settings → API tokens.");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function api(path) {
|
|
33
|
+
const res = await fetch(`${BASE_URL}${path}`, {
|
|
34
|
+
headers: { Authorization: `Bearer ${TOKEN}`, Accept: "application/json" },
|
|
35
|
+
});
|
|
36
|
+
if (res.status === 401) throw new Error("AIStorageDepot rejected the token (401) — check AISD_TOKEN.");
|
|
37
|
+
if (!res.ok) throw new Error(`AIStorageDepot ${path} → HTTP ${res.status}`);
|
|
38
|
+
return res.json();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Library snapshot, cached briefly so a burst of list/read calls hits the API once.
|
|
42
|
+
let cache = { at: 0, workspaces: [], items: [] };
|
|
43
|
+
async function snapshot() {
|
|
44
|
+
if (cache.items.length && Date.now() - cache.at < 30_000) return cache;
|
|
45
|
+
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
|
+
}
|
|
55
|
+
cache = { at: Date.now(), workspaces, items };
|
|
56
|
+
return cache;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- mapping helpers ---
|
|
60
|
+
const slug = (s) =>
|
|
61
|
+
String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "item";
|
|
62
|
+
const isPrompt = (it) => it.type?.format === "prompt" || (it.placeholders?.length ?? 0) > 0;
|
|
63
|
+
const mimeFor = (it) => (it.type?.format === "json" ? "application/json" : "text/markdown");
|
|
64
|
+
|
|
65
|
+
// Stable, unique prompt name per item (slug of the title, deduped with an id suffix).
|
|
66
|
+
// Rebuilt deterministically from the snapshot so ListPrompts and GetPrompt agree.
|
|
67
|
+
function promptIndex(items) {
|
|
68
|
+
const used = new Set();
|
|
69
|
+
const byName = new Map();
|
|
70
|
+
for (const it of items.filter(isPrompt)) {
|
|
71
|
+
let name = slug(it.title);
|
|
72
|
+
if (used.has(name)) name = `${name}-${it.id.slice(-4)}`;
|
|
73
|
+
used.add(name);
|
|
74
|
+
byName.set(name, it);
|
|
75
|
+
}
|
|
76
|
+
return byName;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MCP argument names should be simple identifiers; map them back to the original
|
|
80
|
+
// "[placeholder]" text so we can substitute into the body.
|
|
81
|
+
function argMap(placeholders) {
|
|
82
|
+
const m = new Map();
|
|
83
|
+
const used = new Set();
|
|
84
|
+
for (const p of placeholders || []) {
|
|
85
|
+
let a = slug(p).replace(/-/g, "_");
|
|
86
|
+
while (used.has(a)) a += "_";
|
|
87
|
+
used.add(a);
|
|
88
|
+
m.set(a, p);
|
|
89
|
+
}
|
|
90
|
+
return m;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function fillBody(body, placeholders, args) {
|
|
94
|
+
let out = body;
|
|
95
|
+
for (const [argName, original] of argMap(placeholders)) {
|
|
96
|
+
const val = args?.[argName];
|
|
97
|
+
if (val != null && val !== "") out = out.split(`[${original}]`).join(val);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const server = new Server(
|
|
103
|
+
{ name: "aistoragedepot", version: "0.1.0" },
|
|
104
|
+
{ capabilities: { resources: {}, prompts: {}, tools: {} } },
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// ---- Resources: every item, readable at aisd://item/<id> ----
|
|
108
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
109
|
+
const { items } = await snapshot();
|
|
110
|
+
return {
|
|
111
|
+
resources: items.map((it) => ({
|
|
112
|
+
uri: `aisd://item/${it.id}`,
|
|
113
|
+
name: it.title,
|
|
114
|
+
description: `${it.type?.name || "Item"}${it.tags?.length ? " · " + it.tags.join(", ") : ""}`,
|
|
115
|
+
mimeType: mimeFor(it),
|
|
116
|
+
})),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
121
|
+
const { uri } = req.params;
|
|
122
|
+
const id = uri.replace(/^aisd:\/\/item\//, "");
|
|
123
|
+
const { items } = await snapshot();
|
|
124
|
+
const it = items.find((x) => x.id === id);
|
|
125
|
+
if (!it) throw new Error(`Unknown resource: ${uri}`);
|
|
126
|
+
return { contents: [{ uri, mimeType: mimeFor(it), text: it.body }] };
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ---- Prompts: prompt-format items, [fields] → arguments ----
|
|
130
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
131
|
+
const { items } = await snapshot();
|
|
132
|
+
const prompts = [];
|
|
133
|
+
for (const [name, it] of promptIndex(items)) {
|
|
134
|
+
const am = argMap(it.placeholders);
|
|
135
|
+
prompts.push({
|
|
136
|
+
name,
|
|
137
|
+
description: `${it.type?.name || "Prompt"}: “${it.title}” (AIStorageDepot)`,
|
|
138
|
+
arguments: [...am.keys()].map((a) => ({
|
|
139
|
+
name: a,
|
|
140
|
+
description: `Value for [${am.get(a)}]`,
|
|
141
|
+
required: false,
|
|
142
|
+
})),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return { prompts };
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
149
|
+
const { items } = await snapshot();
|
|
150
|
+
const it = promptIndex(items).get(req.params.name);
|
|
151
|
+
if (!it) throw new Error(`Unknown prompt: ${req.params.name}`);
|
|
152
|
+
const text = fillBody(it.body, it.placeholders, req.params.arguments || {});
|
|
153
|
+
return {
|
|
154
|
+
description: it.title,
|
|
155
|
+
messages: [{ role: "user", content: { type: "text", text } }],
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ---- Tools: search + fetch ----
|
|
160
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
161
|
+
tools: [
|
|
162
|
+
{
|
|
163
|
+
name: "search_library",
|
|
164
|
+
description:
|
|
165
|
+
"Search your AIStorageDepot library (prompts, rules, docs, skills, configs) by keyword across titles, bodies, tags, and categories. Returns matching items with their aisd://item/<id> resource URIs.",
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: "object",
|
|
168
|
+
properties: { query: { type: "string", description: "Keyword(s) to search for." } },
|
|
169
|
+
required: ["query"],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "get_item",
|
|
174
|
+
description:
|
|
175
|
+
"Fetch the full content of one library item by its id or aisd://item/<id> URI (e.g. from a search_library result).",
|
|
176
|
+
inputSchema: {
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: { id: { type: "string", description: "The item id, or an aisd://item/<id> URI." } },
|
|
179
|
+
required: ["id"],
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
186
|
+
const { name, arguments: args } = req.params;
|
|
187
|
+
try {
|
|
188
|
+
if (name === "search_library") {
|
|
189
|
+
const q = String(args?.query || "").trim();
|
|
190
|
+
if (q.length < 2) return { content: [{ type: "text", text: "Enter at least 2 characters." }] };
|
|
191
|
+
const { workspaces } = await snapshot();
|
|
192
|
+
const ids = workspaces.map((w) => w.id).join(",");
|
|
193
|
+
const results = await api(`/api/search?q=${encodeURIComponent(q)}&workspaces=${encodeURIComponent(ids)}`);
|
|
194
|
+
if (!results.length) return { content: [{ type: "text", text: `No matches for “${q}”.` }] };
|
|
195
|
+
const lines = results.map(
|
|
196
|
+
(r) => `• ${r.title} — ${r.typeName} (in ${r.workspaceName}) → aisd://item/${r.id}`,
|
|
197
|
+
);
|
|
198
|
+
return { content: [{ type: "text", text: `${results.length} match(es) for “${q}”:\n${lines.join("\n")}` }] };
|
|
199
|
+
}
|
|
200
|
+
if (name === "get_item") {
|
|
201
|
+
const id = String(args?.id || "").replace(/^aisd:\/\/item\//, "");
|
|
202
|
+
const { items } = await snapshot();
|
|
203
|
+
const it = items.find((x) => x.id === id);
|
|
204
|
+
if (!it) return { content: [{ type: "text", text: `No item with id ${id}.` }], isError: true };
|
|
205
|
+
return { content: [{ type: "text", text: it.body }] };
|
|
206
|
+
}
|
|
207
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
208
|
+
} catch (err) {
|
|
209
|
+
return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const transport = new StdioServerTransport();
|
|
214
|
+
await server.connect(transport);
|
|
215
|
+
console.error(`AIStorageDepot MCP server connected → ${BASE_URL}`);
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aistoragedepot/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aistoragedepot-mcp": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test:client": "node test-client.mjs"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"modelcontextprotocol",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"aistoragedepot",
|
|
28
|
+
"prompts",
|
|
29
|
+
"ai",
|
|
30
|
+
"llm",
|
|
31
|
+
"claude",
|
|
32
|
+
"cursor"
|
|
33
|
+
],
|
|
34
|
+
"author": "AIStorageDepot",
|
|
35
|
+
"homepage": "https://www.aistoragedepot.com",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/davidmcolvin/aistoragedepot-mcp.git"
|
|
39
|
+
},
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/davidmcolvin/aistoragedepot-mcp/issues"
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|