@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.
Files changed (3) hide show
  1. package/README.md +10 -2
  2. package/package.json +1 -1
  3. 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 paste corpus by keyword. |
20
- | `pastepile_list` | List the most recent public pastes. |
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.0",
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.0";
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. Search the public corpus " +
26
- "with pastepile_search. Saved pastes default to unlisted (retrievable by " +
27
- "URL, not shown in public listings or search).";
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 public Pastepile pastes by keyword and get back matching " +
105
- "titles, previews, and URLs. This searches the global public corpus of " +
106
- "listed pastes, not a private per-user store. Use it to find existing " +
107
- "public snippets or reference material before writing from scratch.",
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 public paste titles and content. Required.",
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 the most recent public Pastepile pastes. Returns slugs, titles, " +
130
- "and URLs. Use this to browse what has been shared publicly; for " +
131
- "targeted lookups use pastepile_search instead.",
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
- const res = await apiFetch(deps, "POST", "/api/public/pastes", body);
229
- const data = await readJson(res);
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
- return {
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
- return {
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
  };