@promptingbox/mcp 0.1.3 → 0.2.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.
@@ -15,6 +15,11 @@ export interface SavePromptResult {
15
15
  createdAt: string;
16
16
  url: string;
17
17
  }
18
+ export interface AccountInfo {
19
+ id: string;
20
+ email: string;
21
+ name: string;
22
+ }
18
23
  export interface Folder {
19
24
  id: string;
20
25
  name: string;
@@ -29,6 +34,58 @@ export interface PromptListItem {
29
34
  title: string;
30
35
  folderId: string | null;
31
36
  folderName: string | null;
37
+ isFavorite?: boolean;
38
+ }
39
+ export interface PromptDetail {
40
+ id: string;
41
+ title: string;
42
+ content: string;
43
+ folderId: string | null;
44
+ folderName: string | null;
45
+ isFavorite: boolean;
46
+ tags: Tag[];
47
+ createdAt: string;
48
+ updatedAt: string;
49
+ }
50
+ export interface PromptVersion {
51
+ id: string;
52
+ versionNumber: number;
53
+ title: string;
54
+ content: string;
55
+ versionNote: string | null;
56
+ createdAt: string;
57
+ }
58
+ export interface TemplateListItem {
59
+ id: string;
60
+ title: string;
61
+ description: string | null;
62
+ category: string;
63
+ icon: string | null;
64
+ usageCount: number;
65
+ }
66
+ export interface TemplateDetail {
67
+ id: string;
68
+ title: string;
69
+ content: string;
70
+ description: string | null;
71
+ category: string;
72
+ icon: string | null;
73
+ usageCount: number;
74
+ }
75
+ export interface TemplateSearchResult {
76
+ templates: TemplateListItem[];
77
+ pagination: {
78
+ page: number;
79
+ limit: number;
80
+ total: number;
81
+ hasMore: boolean;
82
+ };
83
+ }
84
+ export interface SearchPromptsParams {
85
+ search?: string;
86
+ tag?: string;
87
+ folder?: string;
88
+ favorites?: boolean;
32
89
  }
33
90
  export declare class PromptingBoxClient {
34
91
  private apiKey;
@@ -42,4 +99,63 @@ export declare class PromptingBoxClient {
42
99
  movePromptToFolder(promptId: string, folder: string): Promise<{
43
100
  success: boolean;
44
101
  }>;
102
+ getAccountInfo(): Promise<AccountInfo>;
103
+ getPrompt(id: string): Promise<PromptDetail>;
104
+ searchPrompts(params: SearchPromptsParams): Promise<PromptListItem[]>;
105
+ updatePrompt(id: string, updates: {
106
+ title?: string;
107
+ content?: string;
108
+ }): Promise<{
109
+ success: boolean;
110
+ id: string;
111
+ versionCreated: boolean;
112
+ newVersionNumber: number | null;
113
+ }>;
114
+ deletePrompt(id: string): Promise<{
115
+ success: boolean;
116
+ }>;
117
+ duplicatePrompt(id: string): Promise<{
118
+ id: string;
119
+ title: string;
120
+ url: string;
121
+ }>;
122
+ toggleFavorite(id: string, isFavorite: boolean): Promise<{
123
+ success: boolean;
124
+ isFavorite: boolean;
125
+ }>;
126
+ updatePromptTags(promptId: string, tagNames: string[]): Promise<{
127
+ success: boolean;
128
+ tags: Tag[];
129
+ }>;
130
+ deleteTag(id: string): Promise<{
131
+ success: boolean;
132
+ tagName: string;
133
+ }>;
134
+ createFolder(name: string): Promise<{
135
+ id: string;
136
+ name: string;
137
+ alreadyExisted: boolean;
138
+ }>;
139
+ deleteFolder(id: string): Promise<{
140
+ success: boolean;
141
+ folderName: string;
142
+ }>;
143
+ listVersions(promptId: string): Promise<PromptVersion[]>;
144
+ restoreVersion(promptId: string, versionNumber: number): Promise<{
145
+ success: boolean;
146
+ restoredVersion: number;
147
+ newVersionNumber: number;
148
+ }>;
149
+ searchTemplates(params?: {
150
+ search?: string;
151
+ category?: string;
152
+ limit?: number;
153
+ page?: number;
154
+ }): Promise<TemplateSearchResult>;
155
+ getTemplate(id: string): Promise<TemplateDetail>;
156
+ useTemplate(templateId: string): Promise<{
157
+ promptId: string;
158
+ title: string;
159
+ url: string;
160
+ }>;
45
161
  }
@@ -22,25 +22,124 @@ export class PromptingBoxClient {
22
22
  }
23
23
  return res.json();
24
24
  }
25
+ // ── Existing methods ─────────────────────────────────────────────────────
25
26
  async savePrompt(params) {
26
- return this.request('/api/mcp/prompts', {
27
+ return this.request('/api/mcp/prompt', {
27
28
  method: 'POST',
28
29
  body: JSON.stringify(params),
29
30
  });
30
31
  }
31
32
  async listFolders() {
32
- return this.request('/api/mcp/folders');
33
+ return this.request('/api/mcp/folder');
33
34
  }
34
35
  async listTags() {
35
- return this.request('/api/mcp/tags');
36
+ return this.request('/api/mcp/tag');
36
37
  }
37
38
  async listPrompts() {
38
- return this.request('/api/mcp/prompts');
39
+ return this.request('/api/mcp/prompt');
39
40
  }
40
41
  async movePromptToFolder(promptId, folder) {
41
- return this.request(`/api/mcp/prompts/${promptId}/folder`, {
42
+ return this.request(`/api/mcp/prompt/${promptId}/folder`, {
42
43
  method: 'PATCH',
43
44
  body: JSON.stringify({ folder }),
44
45
  });
45
46
  }
47
+ async getAccountInfo() {
48
+ return this.request('/api/mcp/me');
49
+ }
50
+ // ── New: Prompt operations ───────────────────────────────────────────────
51
+ async getPrompt(id) {
52
+ return this.request(`/api/mcp/prompt/${id}`);
53
+ }
54
+ async searchPrompts(params) {
55
+ const qs = new URLSearchParams();
56
+ if (params.search)
57
+ qs.set('search', params.search);
58
+ if (params.tag)
59
+ qs.set('tag', params.tag);
60
+ if (params.folder)
61
+ qs.set('folder', params.folder);
62
+ if (params.favorites)
63
+ qs.set('favorites', 'true');
64
+ const query = qs.toString();
65
+ return this.request(`/api/mcp/prompt${query ? `?${query}` : ''}`);
66
+ }
67
+ async updatePrompt(id, updates) {
68
+ return this.request(`/api/mcp/prompt/${id}`, {
69
+ method: 'PATCH',
70
+ body: JSON.stringify(updates),
71
+ });
72
+ }
73
+ async deletePrompt(id) {
74
+ return this.request(`/api/mcp/prompt/${id}`, {
75
+ method: 'DELETE',
76
+ });
77
+ }
78
+ async duplicatePrompt(id) {
79
+ return this.request(`/api/mcp/prompt/${id}/duplicate`, {
80
+ method: 'POST',
81
+ });
82
+ }
83
+ async toggleFavorite(id, isFavorite) {
84
+ return this.request(`/api/mcp/prompt/${id}/favorite`, {
85
+ method: 'PUT',
86
+ body: JSON.stringify({ isFavorite }),
87
+ });
88
+ }
89
+ // ── New: Tag operations ──────────────────────────────────────────────────
90
+ async updatePromptTags(promptId, tagNames) {
91
+ return this.request(`/api/mcp/prompt/${promptId}/tag`, {
92
+ method: 'PUT',
93
+ body: JSON.stringify({ tagNames }),
94
+ });
95
+ }
96
+ async deleteTag(id) {
97
+ return this.request(`/api/mcp/tag/${id}`, {
98
+ method: 'DELETE',
99
+ });
100
+ }
101
+ // ── New: Folder operations ───────────────────────────────────────────────
102
+ async createFolder(name) {
103
+ return this.request('/api/mcp/folder', {
104
+ method: 'POST',
105
+ body: JSON.stringify({ name }),
106
+ });
107
+ }
108
+ async deleteFolder(id) {
109
+ return this.request(`/api/mcp/folder/${id}`, {
110
+ method: 'DELETE',
111
+ });
112
+ }
113
+ // ── New: Version operations ──────────────────────────────────────────────
114
+ async listVersions(promptId) {
115
+ return this.request(`/api/mcp/prompt/${promptId}/version`);
116
+ }
117
+ async restoreVersion(promptId, versionNumber) {
118
+ return this.request(`/api/mcp/prompt/${promptId}/restore`, {
119
+ method: 'POST',
120
+ body: JSON.stringify({ versionNumber }),
121
+ });
122
+ }
123
+ // ── New: Template operations ─────────────────────────────────────────────
124
+ async searchTemplates(params) {
125
+ const qs = new URLSearchParams();
126
+ if (params?.search)
127
+ qs.set('search', params.search);
128
+ if (params?.category)
129
+ qs.set('category', params.category);
130
+ if (params?.limit)
131
+ qs.set('limit', String(params.limit));
132
+ if (params?.page)
133
+ qs.set('page', String(params.page));
134
+ const query = qs.toString();
135
+ return this.request(`/api/mcp/template${query ? `?${query}` : ''}`);
136
+ }
137
+ async getTemplate(id) {
138
+ return this.request(`/api/mcp/template/${id}`);
139
+ }
140
+ async useTemplate(templateId) {
141
+ return this.request(`/api/mcp/template/${templateId}/use`, {
142
+ method: 'POST',
143
+ });
144
+ }
46
145
  }
package/dist/index.js CHANGED
@@ -11,10 +11,45 @@ if (!API_KEY) {
11
11
  process.exit(1);
12
12
  }
13
13
  const client = new PromptingBoxClient({ apiKey: API_KEY, baseUrl: BASE_URL });
14
+ // Cache account info so we can surface it in every response
15
+ let accountEmail = null;
16
+ async function getAccountLabel() {
17
+ if (accountEmail)
18
+ return accountEmail;
19
+ try {
20
+ const info = await client.getAccountInfo();
21
+ accountEmail = info.email;
22
+ return accountEmail;
23
+ }
24
+ catch {
25
+ return 'unknown (could not verify)';
26
+ }
27
+ }
28
+ /** Resolve promptId from either explicit ID or title search */
29
+ async function resolvePromptId(promptId, promptTitle) {
30
+ if (promptId)
31
+ return { id: promptId };
32
+ if (!promptTitle)
33
+ return { error: 'Provide either promptId or promptTitle.' };
34
+ const all = await client.listPrompts();
35
+ const lower = promptTitle.toLowerCase();
36
+ const matches = all.filter((p) => p.title.toLowerCase().includes(lower));
37
+ if (matches.length === 0)
38
+ return { error: `No prompt found matching "${promptTitle}".` };
39
+ if (matches.length > 1) {
40
+ const list = matches.map((p) => `- ${p.title} (id: ${p.id})`).join('\n');
41
+ return { error: `Multiple prompts match "${promptTitle}". Use promptId to be specific:\n${list}` };
42
+ }
43
+ return { id: matches[0].id };
44
+ }
45
+ function errorResult(message) {
46
+ return { content: [{ type: 'text', text: message }], isError: true };
47
+ }
14
48
  const server = new McpServer({
15
49
  name: 'promptingbox',
16
- version: '0.1.0',
50
+ version: '0.2.0',
17
51
  });
52
+ const baseUrl = BASE_URL ?? 'https://www.promptingbox.com';
18
53
  // ── save_prompt ──────────────────────────────────────────────────────────────
19
54
  server.tool('save_prompt', 'Save a prompt to the user\'s PromptingBox account. Use this when the user wants to save, store, or bookmark a prompt.', {
20
55
  title: z.string().describe('A short, descriptive title for the prompt'),
@@ -23,54 +58,447 @@ server.tool('save_prompt', 'Save a prompt to the user\'s PromptingBox account. U
23
58
  tagNames: z.array(z.string()).optional().describe('Tag names to apply (created if they don\'t exist)'),
24
59
  }, async ({ title, content, folder, tagNames }) => {
25
60
  try {
26
- const result = await client.savePrompt({ title, content, folder, tagNames });
61
+ const [result, email] = await Promise.all([
62
+ client.savePrompt({ title, content, folder, tagNames }),
63
+ getAccountLabel(),
64
+ ]);
27
65
  return {
28
66
  content: [
29
67
  {
30
68
  type: 'text',
31
69
  text: `Prompt saved to PromptingBox!\n\nTitle: ${result.title}\nID: ${result.id}\nURL: ${result.url}` +
32
- (result.folderId ? `\nFolder: ${folder}` : ''),
70
+ (result.folderId ? `\nFolder: ${folder}` : '') +
71
+ `\n\n🔑 Account: ${email}`,
33
72
  },
34
73
  ],
35
74
  };
36
75
  }
37
76
  catch (err) {
38
77
  const message = err instanceof Error ? err.message : String(err);
78
+ return errorResult(`Failed to save prompt: ${message}`);
79
+ }
80
+ });
81
+ // ── get_prompt ───────────────────────────────────────────────────────────────
82
+ server.tool('get_prompt', 'Get the full content of a single prompt from PromptingBox. Returns title, content, tags, folder, and metadata.', {
83
+ promptId: z.string().optional().describe('The prompt ID. Provide this or promptTitle.'),
84
+ promptTitle: z.string().optional().describe('The prompt title to search for. Provide this or promptId.'),
85
+ }, async ({ promptId, promptTitle }) => {
86
+ try {
87
+ const resolved = await resolvePromptId(promptId, promptTitle);
88
+ if ('error' in resolved)
89
+ return errorResult(resolved.error);
90
+ const [prompt, email] = await Promise.all([
91
+ client.getPrompt(resolved.id),
92
+ getAccountLabel(),
93
+ ]);
94
+ const tagList = prompt.tags.length > 0
95
+ ? `Tags: ${prompt.tags.map((t) => t.name).join(', ')}`
96
+ : 'Tags: (none)';
97
+ return {
98
+ content: [{
99
+ type: 'text',
100
+ text: `# ${prompt.title}\n\nID: ${prompt.id}\n` +
101
+ `Folder: ${prompt.folderName ?? '(none)'}\n` +
102
+ `${tagList}\n` +
103
+ `Favorite: ${prompt.isFavorite ? 'Yes' : 'No'}\n` +
104
+ `URL: ${baseUrl}/workspace/prompt/${prompt.id}\n\n` +
105
+ `---\n\n${prompt.content}\n\n🔑 Account: ${email}`,
106
+ }],
107
+ };
108
+ }
109
+ catch (err) {
110
+ const message = err instanceof Error ? err.message : String(err);
111
+ return errorResult(`Failed to get prompt: ${message}`);
112
+ }
113
+ });
114
+ // ── search_prompts ──────────────────────────────────────────────────────────
115
+ server.tool('search_prompts', 'Search prompts in PromptingBox by title, content, tag, folder, or favorites. Returns matching prompts.', {
116
+ query: z.string().optional().describe('Search text to match against title and content'),
117
+ tag: z.string().optional().describe('Filter by tag name'),
118
+ folder: z.string().optional().describe('Filter by folder name'),
119
+ favorites: z.boolean().optional().describe('Set to true to only show favorited prompts'),
120
+ }, async ({ query, tag, folder, favorites }) => {
121
+ try {
122
+ const [results, email] = await Promise.all([
123
+ client.searchPrompts({ search: query, tag, folder, favorites }),
124
+ getAccountLabel(),
125
+ ]);
126
+ if (results.length === 0) {
127
+ return {
128
+ content: [{ type: 'text', text: `No prompts found matching your search.\n\n🔑 Account: ${email}` }],
129
+ };
130
+ }
131
+ const lines = results.map((p) => `- ${p.isFavorite ? '⭐ ' : ''}${p.title} (id: \`${p.id}\`)${p.folderName ? ` — 📁 ${p.folderName}` : ''}`);
132
+ return {
133
+ content: [{
134
+ type: 'text',
135
+ text: `Found ${results.length} prompt${results.length === 1 ? '' : 's'}:\n\n${lines.join('\n')}\n\n🔑 Account: ${email}`,
136
+ }],
137
+ };
138
+ }
139
+ catch (err) {
140
+ const message = err instanceof Error ? err.message : String(err);
141
+ return errorResult(`Failed to search prompts: ${message}`);
142
+ }
143
+ });
144
+ // ── update_prompt ───────────────────────────────────────────────────────────
145
+ server.tool('update_prompt', 'Update the title and/or content of an existing prompt. If content changes, a new version is automatically created.', {
146
+ promptId: z.string().optional().describe('The prompt ID. Provide this or promptTitle.'),
147
+ promptTitle: z.string().optional().describe('The prompt title to search for. Provide this or promptId.'),
148
+ title: z.string().optional().describe('New title for the prompt'),
149
+ content: z.string().optional().describe('New content for the prompt'),
150
+ }, async ({ promptId, promptTitle, title, content }) => {
151
+ try {
152
+ if (!title && !content)
153
+ return errorResult('Provide at least a new title or content to update.');
154
+ const resolved = await resolvePromptId(promptId, promptTitle);
155
+ if ('error' in resolved)
156
+ return errorResult(resolved.error);
157
+ const [result, email] = await Promise.all([
158
+ client.updatePrompt(resolved.id, { title, content }),
159
+ getAccountLabel(),
160
+ ]);
161
+ let text = `Prompt updated successfully!\nID: ${result.id}`;
162
+ if (result.versionCreated) {
163
+ text += `\nNew version created: v${result.newVersionNumber}`;
164
+ }
165
+ text += `\nURL: ${baseUrl}/workspace/prompt/${result.id}`;
166
+ text += `\n\n🔑 Account: ${email}`;
167
+ return { content: [{ type: 'text', text }] };
168
+ }
169
+ catch (err) {
170
+ const message = err instanceof Error ? err.message : String(err);
171
+ return errorResult(`Failed to update prompt: ${message}`);
172
+ }
173
+ });
174
+ // ── delete_prompt ───────────────────────────────────────────────────────────
175
+ server.tool('delete_prompt', 'Permanently delete a prompt from PromptingBox. This also deletes all versions and tag associations.', {
176
+ promptId: z.string().optional().describe('The prompt ID. Provide this or promptTitle.'),
177
+ promptTitle: z.string().optional().describe('The prompt title to search for. Provide this or promptId.'),
178
+ }, async ({ promptId, promptTitle }) => {
179
+ try {
180
+ const resolved = await resolvePromptId(promptId, promptTitle);
181
+ if ('error' in resolved)
182
+ return errorResult(resolved.error);
183
+ await client.deletePrompt(resolved.id);
184
+ const email = await getAccountLabel();
185
+ return {
186
+ content: [{ type: 'text', text: `Prompt deleted successfully.\n\n🔑 Account: ${email}` }],
187
+ };
188
+ }
189
+ catch (err) {
190
+ const message = err instanceof Error ? err.message : String(err);
191
+ return errorResult(`Failed to delete prompt: ${message}`);
192
+ }
193
+ });
194
+ // ── duplicate_prompt ────────────────────────────────────────────────────────
195
+ server.tool('duplicate_prompt', 'Create a copy of an existing prompt. The copy gets "(Copy)" appended to the title and inherits the same folder and tags.', {
196
+ promptId: z.string().optional().describe('The prompt ID. Provide this or promptTitle.'),
197
+ promptTitle: z.string().optional().describe('The prompt title to search for. Provide this or promptId.'),
198
+ }, async ({ promptId, promptTitle }) => {
199
+ try {
200
+ const resolved = await resolvePromptId(promptId, promptTitle);
201
+ if ('error' in resolved)
202
+ return errorResult(resolved.error);
203
+ const [result, email] = await Promise.all([
204
+ client.duplicatePrompt(resolved.id),
205
+ getAccountLabel(),
206
+ ]);
207
+ return {
208
+ content: [{
209
+ type: 'text',
210
+ text: `Prompt duplicated!\n\nTitle: ${result.title}\nID: ${result.id}\nURL: ${result.url}\n\n🔑 Account: ${email}`,
211
+ }],
212
+ };
213
+ }
214
+ catch (err) {
215
+ const message = err instanceof Error ? err.message : String(err);
216
+ return errorResult(`Failed to duplicate prompt: ${message}`);
217
+ }
218
+ });
219
+ // ── toggle_favorite ─────────────────────────────────────────────────────────
220
+ server.tool('toggle_favorite', 'Star or unstar a prompt in PromptingBox.', {
221
+ promptId: z.string().optional().describe('The prompt ID. Provide this or promptTitle.'),
222
+ promptTitle: z.string().optional().describe('The prompt title to search for. Provide this or promptId.'),
223
+ isFavorite: z.boolean().describe('true to favorite, false to unfavorite'),
224
+ }, async ({ promptId, promptTitle, isFavorite }) => {
225
+ try {
226
+ const resolved = await resolvePromptId(promptId, promptTitle);
227
+ if ('error' in resolved)
228
+ return errorResult(resolved.error);
229
+ await client.toggleFavorite(resolved.id, isFavorite);
230
+ const email = await getAccountLabel();
231
+ const action = isFavorite ? 'favorited ⭐' : 'unfavorited';
232
+ return {
233
+ content: [{ type: 'text', text: `Prompt ${action}.\n\n🔑 Account: ${email}` }],
234
+ };
235
+ }
236
+ catch (err) {
237
+ const message = err instanceof Error ? err.message : String(err);
238
+ return errorResult(`Failed to toggle favorite: ${message}`);
239
+ }
240
+ });
241
+ // ── add_tags ────────────────────────────────────────────────────────────────
242
+ server.tool('add_tags', 'Set tags on a prompt (by tag name). This replaces all existing tags on the prompt. Tags are auto-created if they don\'t exist.', {
243
+ promptId: z.string().optional().describe('The prompt ID. Provide this or promptTitle.'),
244
+ promptTitle: z.string().optional().describe('The prompt title to search for. Provide this or promptId.'),
245
+ tagNames: z.array(z.string()).describe('Tag names to set on the prompt (replaces existing tags)'),
246
+ }, async ({ promptId, promptTitle, tagNames }) => {
247
+ try {
248
+ const resolved = await resolvePromptId(promptId, promptTitle);
249
+ if ('error' in resolved)
250
+ return errorResult(resolved.error);
251
+ const [result, email] = await Promise.all([
252
+ client.updatePromptTags(resolved.id, tagNames),
253
+ getAccountLabel(),
254
+ ]);
255
+ const tagList = result.tags.map((t) => t.name).join(', ');
256
+ return {
257
+ content: [{
258
+ type: 'text',
259
+ text: `Tags updated: ${tagList || '(no tags)'}.\n\n🔑 Account: ${email}`,
260
+ }],
261
+ };
262
+ }
263
+ catch (err) {
264
+ const message = err instanceof Error ? err.message : String(err);
265
+ return errorResult(`Failed to update tags: ${message}`);
266
+ }
267
+ });
268
+ // ── delete_tag ──────────────────────────────────────────────────────────────
269
+ server.tool('delete_tag', 'Delete a tag entirely from PromptingBox. Removes it from all prompts that use it.', {
270
+ tagId: z.string().optional().describe('The tag ID. Provide this or tagName.'),
271
+ tagName: z.string().optional().describe('The tag name. Provide this or tagId.'),
272
+ }, async ({ tagId, tagName }) => {
273
+ try {
274
+ let resolvedId = tagId;
275
+ if (!resolvedId) {
276
+ if (!tagName)
277
+ return errorResult('Provide either tagId or tagName.');
278
+ const allTags = await client.listTags();
279
+ const lower = tagName.toLowerCase();
280
+ const matches = allTags.filter((t) => t.name.toLowerCase() === lower);
281
+ if (matches.length === 0)
282
+ return errorResult(`No tag found matching "${tagName}".`);
283
+ resolvedId = matches[0].id;
284
+ }
285
+ const [result, email] = await Promise.all([
286
+ client.deleteTag(resolvedId),
287
+ getAccountLabel(),
288
+ ]);
289
+ return {
290
+ content: [{ type: 'text', text: `Tag "${result.tagName}" deleted.\n\n🔑 Account: ${email}` }],
291
+ };
292
+ }
293
+ catch (err) {
294
+ const message = err instanceof Error ? err.message : String(err);
295
+ return errorResult(`Failed to delete tag: ${message}`);
296
+ }
297
+ });
298
+ // ── create_folder ───────────────────────────────────────────────────────────
299
+ server.tool('create_folder', 'Create a new folder in PromptingBox. If a folder with the same name exists, returns the existing one.', {
300
+ name: z.string().describe('The folder name to create'),
301
+ }, async ({ name }) => {
302
+ try {
303
+ const [result, email] = await Promise.all([
304
+ client.createFolder(name),
305
+ getAccountLabel(),
306
+ ]);
307
+ const status = result.alreadyExisted ? 'already exists' : 'created';
308
+ return {
309
+ content: [{
310
+ type: 'text',
311
+ text: `Folder "${result.name}" ${status}.\nID: ${result.id}\n\n🔑 Account: ${email}`,
312
+ }],
313
+ };
314
+ }
315
+ catch (err) {
316
+ const message = err instanceof Error ? err.message : String(err);
317
+ return errorResult(`Failed to create folder: ${message}`);
318
+ }
319
+ });
320
+ // ── delete_folder ───────────────────────────────────────────────────────────
321
+ server.tool('delete_folder', 'Delete a folder from PromptingBox. Prompts in the folder are moved to the root (not deleted).', {
322
+ folderId: z.string().optional().describe('The folder ID. Provide this or folderName.'),
323
+ folderName: z.string().optional().describe('The folder name. Provide this or folderId.'),
324
+ }, async ({ folderId, folderName }) => {
325
+ try {
326
+ let resolvedId = folderId;
327
+ if (!resolvedId) {
328
+ if (!folderName)
329
+ return errorResult('Provide either folderId or folderName.');
330
+ const allFolders = await client.listFolders();
331
+ const lower = folderName.toLowerCase();
332
+ const matches = allFolders.filter((f) => f.name.toLowerCase() === lower);
333
+ if (matches.length === 0)
334
+ return errorResult(`No folder found matching "${folderName}".`);
335
+ resolvedId = matches[0].id;
336
+ }
337
+ const [result, email] = await Promise.all([
338
+ client.deleteFolder(resolvedId),
339
+ getAccountLabel(),
340
+ ]);
341
+ return {
342
+ content: [{
343
+ type: 'text',
344
+ text: `Folder "${result.folderName}" deleted. Prompts moved to root.\n\n🔑 Account: ${email}`,
345
+ }],
346
+ };
347
+ }
348
+ catch (err) {
349
+ const message = err instanceof Error ? err.message : String(err);
350
+ return errorResult(`Failed to delete folder: ${message}`);
351
+ }
352
+ });
353
+ // ── list_versions ───────────────────────────────────────────────────────────
354
+ server.tool('list_versions', 'Get the version history for a prompt. Shows all saved versions with their version numbers, notes, and timestamps.', {
355
+ promptId: z.string().optional().describe('The prompt ID. Provide this or promptTitle.'),
356
+ promptTitle: z.string().optional().describe('The prompt title to search for. Provide this or promptId.'),
357
+ }, async ({ promptId, promptTitle }) => {
358
+ try {
359
+ const resolved = await resolvePromptId(promptId, promptTitle);
360
+ if ('error' in resolved)
361
+ return errorResult(resolved.error);
362
+ const [versions, email] = await Promise.all([
363
+ client.listVersions(resolved.id),
364
+ getAccountLabel(),
365
+ ]);
366
+ if (versions.length === 0) {
367
+ return {
368
+ content: [{ type: 'text', text: `No versions found.\n\n🔑 Account: ${email}` }],
369
+ };
370
+ }
371
+ const lines = versions.map((v) => `- **v${v.versionNumber}** — ${v.versionNote ?? 'No note'} (${new Date(v.createdAt).toLocaleDateString()})`);
372
+ return {
373
+ content: [{
374
+ type: 'text',
375
+ text: `Version history (${versions.length} version${versions.length === 1 ? '' : 's'}):\n\n${lines.join('\n')}\n\n🔑 Account: ${email}`,
376
+ }],
377
+ };
378
+ }
379
+ catch (err) {
380
+ const message = err instanceof Error ? err.message : String(err);
381
+ return errorResult(`Failed to list versions: ${message}`);
382
+ }
383
+ });
384
+ // ── restore_version ─────────────────────────────────────────────────────────
385
+ server.tool('restore_version', 'Restore a prompt to a previous version. Creates a new version with the restored content.', {
386
+ promptId: z.string().optional().describe('The prompt ID. Provide this or promptTitle.'),
387
+ promptTitle: z.string().optional().describe('The prompt title to search for. Provide this or promptId.'),
388
+ versionNumber: z.number().int().positive().describe('The version number to restore to'),
389
+ }, async ({ promptId, promptTitle, versionNumber }) => {
390
+ try {
391
+ const resolved = await resolvePromptId(promptId, promptTitle);
392
+ if ('error' in resolved)
393
+ return errorResult(resolved.error);
394
+ const [result, email] = await Promise.all([
395
+ client.restoreVersion(resolved.id, versionNumber),
396
+ getAccountLabel(),
397
+ ]);
398
+ return {
399
+ content: [{
400
+ type: 'text',
401
+ text: `Restored to version ${result.restoredVersion}. New version created: v${result.newVersionNumber}.\n\n🔑 Account: ${email}`,
402
+ }],
403
+ };
404
+ }
405
+ catch (err) {
406
+ const message = err instanceof Error ? err.message : String(err);
407
+ return errorResult(`Failed to restore version: ${message}`);
408
+ }
409
+ });
410
+ // ── search_templates ────────────────────────────────────────────────────────
411
+ server.tool('search_templates', 'Browse and search the PromptingBox public template library. Find pre-built prompts you can save to your collection.', {
412
+ query: z.string().optional().describe('Search text to match against template titles and descriptions'),
413
+ category: z.string().optional().describe('Filter by category (e.g. "Business", "Writing", "Development")'),
414
+ limit: z.number().int().min(1).max(50).optional().default(10).describe('Number of results to return (default 10)'),
415
+ }, async ({ query, category, limit }) => {
416
+ try {
417
+ const result = await client.searchTemplates({ search: query, category, limit });
418
+ if (result.templates.length === 0) {
419
+ return {
420
+ content: [{ type: 'text', text: 'No templates found matching your search.' }],
421
+ };
422
+ }
423
+ const lines = result.templates.map((t) => `- **${t.title}** (${t.category})${t.description ? ` — ${t.description}` : ''}\n ID: \`${t.id}\` | Used ${t.usageCount} times`);
424
+ return {
425
+ content: [{
426
+ type: 'text',
427
+ text: `Found ${result.pagination.total} template${result.pagination.total === 1 ? '' : 's'}` +
428
+ `${result.pagination.hasMore ? ` (showing first ${result.templates.length})` : ''}:\n\n${lines.join('\n\n')}` +
429
+ `\n\nUse \`use_template\` with the template ID to save one to your collection.`,
430
+ }],
431
+ };
432
+ }
433
+ catch (err) {
434
+ const message = err instanceof Error ? err.message : String(err);
435
+ return errorResult(`Failed to search templates: ${message}`);
436
+ }
437
+ });
438
+ // ── use_template ────────────────────────────────────────────────────────────
439
+ server.tool('use_template', 'Save a public template to your PromptingBox collection. Creates a copy you can edit and customize.', {
440
+ templateId: z.string().describe('The template ID (from search_templates)'),
441
+ }, async ({ templateId }) => {
442
+ try {
443
+ const [result, email] = await Promise.all([
444
+ client.useTemplate(templateId),
445
+ getAccountLabel(),
446
+ ]);
39
447
  return {
40
- content: [{ type: 'text', text: `Failed to save prompt: ${message}` }],
41
- isError: true,
448
+ content: [{
449
+ type: 'text',
450
+ text: `Template saved to your collection!\n\nTitle: ${result.title}\nID: ${result.promptId}\nURL: ${result.url}\n\n🔑 Account: ${email}`,
451
+ }],
42
452
  };
43
453
  }
454
+ catch (err) {
455
+ const message = err instanceof Error ? err.message : String(err);
456
+ return errorResult(`Failed to use template: ${message}`);
457
+ }
458
+ });
459
+ // ── whoami ──────────────────────────────────────────────────────────────────
460
+ server.tool('whoami', 'Show which PromptingBox account is connected to this MCP server.', {}, async () => {
461
+ try {
462
+ const info = await client.getAccountInfo();
463
+ accountEmail = info.email; // refresh cache
464
+ return {
465
+ content: [{
466
+ type: 'text',
467
+ text: `Connected to PromptingBox as:\n\nEmail: ${info.email}\nName: ${info.name || '(not set)'}\nID: ${info.id}`,
468
+ }],
469
+ };
470
+ }
471
+ catch (err) {
472
+ const message = err instanceof Error ? err.message : String(err);
473
+ return errorResult(`Failed to get account info: ${message}`);
474
+ }
44
475
  });
45
476
  // ── list_folders ─────────────────────────────────────────────────────────────
46
477
  server.tool('list_folders', 'List all folders in the user\'s PromptingBox account. Useful to know where to save a prompt.', {}, async () => {
47
478
  try {
48
- const folders = await client.listFolders();
479
+ const [folders, email] = await Promise.all([client.listFolders(), getAccountLabel()]);
49
480
  if (folders.length === 0) {
50
481
  return {
51
- content: [{ type: 'text', text: 'No folders found. You can specify a folder name when saving and it will be created automatically.' }],
482
+ content: [{ type: 'text', text: `No folders found. You can specify a folder name when saving and it will be created automatically.\n\n🔑 Account: ${email}` }],
52
483
  };
53
484
  }
54
- const list = folders.map((f) => `- ${f.name}`).join('\n');
485
+ const list = folders.map((f) => `- ${f.name} (id: \`${f.id}\`)`).join('\n');
55
486
  return {
56
- content: [{ type: 'text', text: `Folders in PromptingBox:\n${list}` }],
487
+ content: [{ type: 'text', text: `Folders in PromptingBox:\n${list}\n\n🔑 Account: ${email}` }],
57
488
  };
58
489
  }
59
490
  catch (err) {
60
491
  const message = err instanceof Error ? err.message : String(err);
61
- return {
62
- content: [{ type: 'text', text: `Failed to list folders: ${message}` }],
63
- isError: true,
64
- };
492
+ return errorResult(`Failed to list folders: ${message}`);
65
493
  }
66
494
  });
67
495
  // ── list_prompts ─────────────────────────────────────────────────────────────
68
496
  server.tool('list_prompts', 'List all prompts in the user\'s PromptingBox account grouped by folder. Use this to see what prompts exist and where they are organized.', {}, async () => {
69
497
  try {
70
- const prompts = await client.listPrompts();
498
+ const [prompts, email] = await Promise.all([client.listPrompts(), getAccountLabel()]);
71
499
  if (prompts.length === 0) {
72
500
  return {
73
- content: [{ type: 'text', text: 'No prompts found.' }],
501
+ content: [{ type: 'text', text: `No prompts found.\n\n🔑 Account: ${email}` }],
74
502
  };
75
503
  }
76
504
  // Group by folder
@@ -89,25 +517,23 @@ server.tool('list_prompts', 'List all prompts in the user\'s PromptingBox accoun
89
517
  return -1;
90
518
  return a.localeCompare(b);
91
519
  });
92
- const baseUrl = BASE_URL ?? 'https://www.promptingbox.com';
93
520
  const lines = [`Your PromptingBox prompts (${prompts.length} total):\n`];
94
521
  for (const key of sortedKeys) {
95
522
  lines.push(`📁 ${key}`);
96
523
  for (const p of grouped.get(key)) {
97
- lines.push(` • [${p.title}](${baseUrl}/workspace/prompts/${p.id}) \`${p.id}\``);
524
+ const fav = p.isFavorite ? '⭐ ' : '';
525
+ lines.push(` • ${fav}[${p.title}](${baseUrl}/workspace/prompt/${p.id}) \`${p.id}\``);
98
526
  }
99
527
  lines.push('');
100
528
  }
529
+ lines.push(`🔑 Account: ${email}`);
101
530
  return {
102
531
  content: [{ type: 'text', text: lines.join('\n').trimEnd() }],
103
532
  };
104
533
  }
105
534
  catch (err) {
106
535
  const message = err instanceof Error ? err.message : String(err);
107
- return {
108
- content: [{ type: 'text', text: `Failed to list prompts: ${message}` }],
109
- isError: true,
110
- };
536
+ return errorResult(`Failed to list prompts: ${message}`);
111
537
  }
112
538
  });
113
539
  // ── move_prompt_to_folder ─────────────────────────────────────────────────
@@ -117,65 +543,37 @@ server.tool('move_prompt_to_folder', 'Move a prompt to a different folder. Provi
117
543
  folder: z.string().describe('The folder name to move the prompt into'),
118
544
  }, async ({ promptId, promptTitle, folder }) => {
119
545
  try {
120
- let resolvedId = promptId;
121
- if (!resolvedId) {
122
- if (!promptTitle) {
123
- return {
124
- content: [{ type: 'text', text: 'Provide either promptId or promptTitle.' }],
125
- isError: true,
126
- };
127
- }
128
- const all = await client.listPrompts();
129
- const lower = promptTitle.toLowerCase();
130
- const matches = all.filter((p) => p.title.toLowerCase().includes(lower));
131
- if (matches.length === 0) {
132
- return {
133
- content: [{ type: 'text', text: `No prompt found matching "${promptTitle}".` }],
134
- isError: true,
135
- };
136
- }
137
- if (matches.length > 1) {
138
- const list = matches.map((p) => `- ${p.title} (id: ${p.id})`).join('\n');
139
- return {
140
- content: [{ type: 'text', text: `Multiple prompts match "${promptTitle}". Use promptId to be specific:\n${list}` }],
141
- isError: true,
142
- };
143
- }
144
- resolvedId = matches[0].id;
145
- }
146
- await client.movePromptToFolder(resolvedId, folder);
546
+ const resolved = await resolvePromptId(promptId, promptTitle);
547
+ if ('error' in resolved)
548
+ return errorResult(resolved.error);
549
+ await client.movePromptToFolder(resolved.id, folder);
550
+ const email = await getAccountLabel();
147
551
  return {
148
- content: [{ type: 'text', text: `Moved prompt to folder "${folder}".` }],
552
+ content: [{ type: 'text', text: `Moved prompt to folder "${folder}".\n\n🔑 Account: ${email}` }],
149
553
  };
150
554
  }
151
555
  catch (err) {
152
556
  const message = err instanceof Error ? err.message : String(err);
153
- return {
154
- content: [{ type: 'text', text: `Failed to move prompt: ${message}` }],
155
- isError: true,
156
- };
557
+ return errorResult(`Failed to move prompt: ${message}`);
157
558
  }
158
559
  });
159
560
  // ── list_tags ────────────────────────────────────────────────────────────────
160
561
  server.tool('list_tags', 'List all tags in the user\'s PromptingBox account. Useful to know what tags are available when saving a prompt.', {}, async () => {
161
562
  try {
162
- const tags = await client.listTags();
563
+ const [tags, email] = await Promise.all([client.listTags(), getAccountLabel()]);
163
564
  if (tags.length === 0) {
164
565
  return {
165
- content: [{ type: 'text', text: 'No tags found. You can specify tag names when saving and they will be created automatically.' }],
566
+ content: [{ type: 'text', text: `No tags found. You can specify tag names when saving and they will be created automatically.\n\n🔑 Account: ${email}` }],
166
567
  };
167
568
  }
168
- const list = tags.map((t) => `- ${t.name}`).join('\n');
569
+ const list = tags.map((t) => `- ${t.name} (id: \`${t.id}\`)`).join('\n');
169
570
  return {
170
- content: [{ type: 'text', text: `Tags in PromptingBox:\n${list}` }],
571
+ content: [{ type: 'text', text: `Tags in PromptingBox:\n${list}\n\n🔑 Account: ${email}` }],
171
572
  };
172
573
  }
173
574
  catch (err) {
174
575
  const message = err instanceof Error ? err.message : String(err);
175
- return {
176
- content: [{ type: 'text', text: `Failed to list tags: ${message}` }],
177
- isError: true,
178
- };
576
+ return errorResult(`Failed to list tags: ${message}`);
179
577
  }
180
578
  });
181
579
  // ── start server ─────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promptingbox/mcp",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for PromptingBox — save prompts from Claude, Cursor, and ChatGPT",
5
5
  "license": "MIT",
6
6
  "type": "module",