@pastepile/mcp 0.1.0 → 0.1.1
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 +10 -2
- package/package.json +1 -1
- package/src/server.mjs +87 -17
package/README.md
CHANGED
|
@@ -16,8 +16,16 @@ so it adds no supply-chain surface of its own.
|
|
|
16
16
|
| ------------------ | ------------------------------------------------------------------- |
|
|
17
17
|
| `pastepile_save` | Save text/code and get back a stable `url`, `slug`, and `edit_key`. |
|
|
18
18
|
| `pastepile_get` | Retrieve a paste by slug or full Pastepile URL. |
|
|
19
|
-
| `pastepile_search` | Search the public
|
|
20
|
-
| `pastepile_list` | List
|
|
19
|
+
| `pastepile_search` | Search your private memory (with an API key) or the public corpus. |
|
|
20
|
+
| `pastepile_list` | List your saved pastes (with an API key) or the public feed. |
|
|
21
|
+
|
|
22
|
+
**Private memory:** when `PASTEPILE_API_KEY` is set, every paste your
|
|
23
|
+
assistant saves is associated with that key, and `pastepile_search` /
|
|
24
|
+
`pastepile_list` default to that private namespace (including unlisted
|
|
25
|
+
pastes), searching titles, filenames, and contents. Pass `scope: "public"`
|
|
26
|
+
to reach the public corpus instead. Without a key, both tools cover the
|
|
27
|
+
public corpus only, and nothing is associated. One key can never see
|
|
28
|
+
another key's pastes.
|
|
21
29
|
|
|
22
30
|
Saved pastes default to **unlisted** (retrievable by URL, never shown in public
|
|
23
31
|
listings or search) and **never expire**, so stored knowledge persists. Both are
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pastepile/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Model Context Protocol server for Pastepile - durable, URL-addressable memory for AI coding assistants. Save and retrieve knowledge from Claude, Cursor, Windsurf, and any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/server.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// returns the response object, or null for a notification that must not be
|
|
13
13
|
// answered.
|
|
14
14
|
|
|
15
|
-
export const VERSION = "0.1.
|
|
15
|
+
export const VERSION = "0.1.1";
|
|
16
16
|
export const PROTOCOL_VERSION = "2025-06-18";
|
|
17
17
|
export const SERVER_NAME = "pastepile";
|
|
18
18
|
export const DEFAULT_BASE = "https://pastepile.com";
|
|
@@ -22,9 +22,11 @@ export const INSTRUCTIONS =
|
|
|
22
22
|
"Pastepile is durable, URL-addressable storage for AI assistants. When you " +
|
|
23
23
|
"produce something worth keeping across sessions (a design, a snippet, a " +
|
|
24
24
|
"log, a decision record), call pastepile_save and remember the returned " +
|
|
25
|
-
"slug or URL. Reload it later with pastepile_get.
|
|
26
|
-
"
|
|
27
|
-
"
|
|
25
|
+
"slug or URL. Reload it later with pastepile_get. When an API key is " +
|
|
26
|
+
"configured, pastepile_search and pastepile_list default to YOUR private " +
|
|
27
|
+
"memory: the pastes saved with that key, including unlisted ones. Without " +
|
|
28
|
+
"a key they cover the public corpus. Saved pastes default to unlisted " +
|
|
29
|
+
"(retrievable by URL, not shown in public listings or search).";
|
|
28
30
|
|
|
29
31
|
const EXPIRY_VALUES = ["10m", "1h", "1d", "1w", "1mo", "never", "burn"];
|
|
30
32
|
const VISIBILITY_VALUES = ["public", "unlisted"];
|
|
@@ -101,16 +103,26 @@ export const TOOLS = [
|
|
|
101
103
|
{
|
|
102
104
|
name: "pastepile_search",
|
|
103
105
|
description:
|
|
104
|
-
"Search
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
106
|
+
"Search Pastepile pastes by keyword and get back matching titles, " +
|
|
107
|
+
"previews, and URLs. With an API key configured this defaults to " +
|
|
108
|
+
'scope "mine": YOUR private memory, the pastes saved with that key ' +
|
|
109
|
+
"(titles, filenames, and contents, including unlisted pastes). Use " +
|
|
110
|
+
'scope "public" to search the global public corpus instead. Without ' +
|
|
111
|
+
"a key only the public scope is available.",
|
|
108
112
|
inputSchema: {
|
|
109
113
|
type: "object",
|
|
110
114
|
properties: {
|
|
111
115
|
query: {
|
|
112
116
|
type: "string",
|
|
113
|
-
description: "Keywords to match against
|
|
117
|
+
description: "Keywords to match against paste titles and content. Required.",
|
|
118
|
+
},
|
|
119
|
+
scope: {
|
|
120
|
+
type: "string",
|
|
121
|
+
enum: ["mine", "public"],
|
|
122
|
+
description:
|
|
123
|
+
'"mine" searches pastes saved with the configured API key ' +
|
|
124
|
+
'(default when a key is set); "public" searches the public ' +
|
|
125
|
+
"corpus (default without a key).",
|
|
114
126
|
},
|
|
115
127
|
limit: {
|
|
116
128
|
type: "integer",
|
|
@@ -126,12 +138,21 @@ export const TOOLS = [
|
|
|
126
138
|
{
|
|
127
139
|
name: "pastepile_list",
|
|
128
140
|
description:
|
|
129
|
-
"List
|
|
130
|
-
|
|
131
|
-
"
|
|
141
|
+
"List recent Pastepile pastes. With an API key configured this " +
|
|
142
|
+
'defaults to scope "mine": YOUR saved pastes, newest first, including ' +
|
|
143
|
+
'unlisted ones. Use scope "public" for the public feed. For targeted ' +
|
|
144
|
+
"lookups use pastepile_search instead.",
|
|
132
145
|
inputSchema: {
|
|
133
146
|
type: "object",
|
|
134
147
|
properties: {
|
|
148
|
+
scope: {
|
|
149
|
+
type: "string",
|
|
150
|
+
enum: ["mine", "public"],
|
|
151
|
+
description:
|
|
152
|
+
'"mine" lists pastes saved with the configured API key (default ' +
|
|
153
|
+
'when a key is set); "public" lists the public feed (default ' +
|
|
154
|
+
"without a key).",
|
|
155
|
+
},
|
|
135
156
|
limit: {
|
|
136
157
|
type: "integer",
|
|
137
158
|
minimum: 1,
|
|
@@ -225,19 +246,43 @@ async function toolSave(deps, args) {
|
|
|
225
246
|
if (typeof args.title === "string" && args.title.length > 0) body.title = args.title;
|
|
226
247
|
if (typeof args.language === "string" && args.language.length > 0) body.language = args.language;
|
|
227
248
|
|
|
228
|
-
|
|
229
|
-
|
|
249
|
+
let res = await apiFetch(deps, "POST", "/api/public/pastes", body);
|
|
250
|
+
let data = await readJson(res);
|
|
251
|
+
let effectiveExpiry = expiry;
|
|
252
|
+
let expiryNote = null;
|
|
253
|
+
// The API reserves no-expiry for pro/enterprise API keys. When our default
|
|
254
|
+
// ("never") hits that gate on a free key, fall back to the longest allowed
|
|
255
|
+
// expiry instead of failing the save, and say so honestly in the result.
|
|
256
|
+
// An EXPLICIT expiry argument is never overridden.
|
|
257
|
+
if (
|
|
258
|
+
!res.ok &&
|
|
259
|
+
args.expiry === undefined &&
|
|
260
|
+
deps.apiKey &&
|
|
261
|
+
data &&
|
|
262
|
+
data.error &&
|
|
263
|
+
/no-expiry/i.test(String(data.error.message || ""))
|
|
264
|
+
) {
|
|
265
|
+
effectiveExpiry = "1mo";
|
|
266
|
+
res = await apiFetch(deps, "POST", "/api/public/pastes", { ...body, expiry: effectiveExpiry });
|
|
267
|
+
data = await readJson(res);
|
|
268
|
+
expiryNote =
|
|
269
|
+
"This API key's plan does not allow never-expiring pastes, so the paste " +
|
|
270
|
+
'was stored with expiry "1mo" instead. Pass expiry explicitly to choose, ' +
|
|
271
|
+
"or upgrade the key for permanent storage.";
|
|
272
|
+
}
|
|
230
273
|
if (!res.ok || !data) throw apiError(res, data);
|
|
231
|
-
|
|
274
|
+
const result = {
|
|
232
275
|
saved: true,
|
|
233
276
|
url: data.url,
|
|
234
277
|
slug: data.slug,
|
|
235
278
|
raw_url: data.raw_url,
|
|
236
279
|
edit_key: data.edit_key,
|
|
237
280
|
visibility,
|
|
238
|
-
expiry,
|
|
281
|
+
expiry: effectiveExpiry,
|
|
239
282
|
note: "Retain edit_key to update or delete this paste later; it is shown only once.",
|
|
240
283
|
};
|
|
284
|
+
if (expiryNote) result.expiry_note = expiryNote;
|
|
285
|
+
return result;
|
|
241
286
|
}
|
|
242
287
|
|
|
243
288
|
async function toolGet(deps, args) {
|
|
@@ -258,7 +303,7 @@ async function toolGet(deps, args) {
|
|
|
258
303
|
}
|
|
259
304
|
|
|
260
305
|
function pickListItem(item) {
|
|
261
|
-
|
|
306
|
+
const out = {
|
|
262
307
|
slug: item.slug,
|
|
263
308
|
title: item.title,
|
|
264
309
|
language: item.language,
|
|
@@ -268,33 +313,58 @@ function pickListItem(item) {
|
|
|
268
313
|
url: item.url,
|
|
269
314
|
raw_url: item.raw_url,
|
|
270
315
|
};
|
|
316
|
+
// Present only on scope=mine results.
|
|
317
|
+
if (item.visibility !== undefined) out.visibility = item.visibility;
|
|
318
|
+
if (item.expires_at !== undefined) out.expires_at = item.expires_at;
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Private memory ("mine") is the default whenever a key is configured; the
|
|
323
|
+
// public corpus is the default (and only option) without one.
|
|
324
|
+
function resolveScope(deps, args) {
|
|
325
|
+
const scope = args.scope === undefined ? (deps.apiKey ? "mine" : "public") : args.scope;
|
|
326
|
+
if (scope !== "mine" && scope !== "public") {
|
|
327
|
+
throw new ToolError('Invalid scope. Use "mine" or "public".');
|
|
328
|
+
}
|
|
329
|
+
if (scope === "mine" && !deps.apiKey) {
|
|
330
|
+
throw new ToolError(
|
|
331
|
+
'scope "mine" requires an API key. Set PASTEPILE_API_KEY in the MCP server config.',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return scope;
|
|
271
335
|
}
|
|
272
336
|
|
|
273
337
|
async function toolSearch(deps, args) {
|
|
274
338
|
if (typeof args.query !== "string" || args.query.trim().length === 0) {
|
|
275
339
|
throw new ToolError("`query` is required and must be a non-empty string.");
|
|
276
340
|
}
|
|
341
|
+
const scope = resolveScope(deps, args);
|
|
277
342
|
const limit = clampLimit(args.limit);
|
|
278
343
|
const qs = new URLSearchParams({ search: args.query.slice(0, 80), limit: String(limit) });
|
|
344
|
+
if (scope === "mine") qs.set("scope", "mine");
|
|
279
345
|
const res = await apiFetch(deps, "GET", `/api/public/pastes?${qs.toString()}`);
|
|
280
346
|
const data = await readJson(res);
|
|
281
347
|
if (!res.ok || !data) throw apiError(res, data);
|
|
282
348
|
const items = Array.isArray(data.items) ? data.items : [];
|
|
283
349
|
return {
|
|
284
350
|
query: args.query,
|
|
351
|
+
scope,
|
|
285
352
|
total: typeof data.total === "number" ? data.total : items.length,
|
|
286
353
|
results: items.map(pickListItem),
|
|
287
354
|
};
|
|
288
355
|
}
|
|
289
356
|
|
|
290
357
|
async function toolList(deps, args) {
|
|
358
|
+
const scope = resolveScope(deps, args);
|
|
291
359
|
const limit = clampLimit(args.limit);
|
|
292
360
|
const qs = new URLSearchParams({ limit: String(limit) });
|
|
361
|
+
if (scope === "mine") qs.set("scope", "mine");
|
|
293
362
|
const res = await apiFetch(deps, "GET", `/api/public/pastes?${qs.toString()}`);
|
|
294
363
|
const data = await readJson(res);
|
|
295
364
|
if (!res.ok || !data) throw apiError(res, data);
|
|
296
365
|
const items = Array.isArray(data.items) ? data.items : [];
|
|
297
366
|
return {
|
|
367
|
+
scope,
|
|
298
368
|
total: typeof data.total === "number" ? data.total : items.length,
|
|
299
369
|
results: items.map(pickListItem),
|
|
300
370
|
};
|