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