@promptprojectmanager/mcp-server 2.8.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/dist/index.js ADDED
@@ -0,0 +1,1010 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import minimist from "minimist";
5
+
6
+ // src/convex-client.ts
7
+ import { ConvexHttpClient } from "convex/browser";
8
+ var PROD_URL = "https://trustworthy-squirrel-735.convex.cloud";
9
+ var DEV_URL = "https://hallowed-shrimp-344.convex.cloud";
10
+ function createConvexClient(isDev) {
11
+ const url = isDev ? DEV_URL : PROD_URL;
12
+ return new ConvexHttpClient(url);
13
+ }
14
+
15
+ // src/server.ts
16
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
17
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18
+ import {
19
+ CallToolRequestSchema,
20
+ GetPromptRequestSchema,
21
+ ListPromptsRequestSchema,
22
+ ListToolsRequestSchema
23
+ } from "@modelcontextprotocol/sdk/types.js";
24
+
25
+ // src/prompt-builder.ts
26
+ function sanitizeForMcp(str) {
27
+ return str.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").trim();
28
+ }
29
+ function buildPromptName(prompt) {
30
+ const workspace = sanitizeForMcp(prompt.workspaceSlug || "default") || "default";
31
+ const promptSlug = sanitizeForMcp(prompt.slug || "unknown") || "unknown";
32
+ return `${workspace}:${promptSlug}`.trim();
33
+ }
34
+ function buildPromptSchema(prompt) {
35
+ return {
36
+ name: buildPromptName(prompt),
37
+ description: prompt.description || `Prompt: ${prompt.name}`,
38
+ arguments: []
39
+ };
40
+ }
41
+ function buildPromptHandler(prompt, config, convexClient) {
42
+ return async () => {
43
+ let flattenedPrompt = prompt.flattenedPrompt;
44
+ let promptName = prompt.name;
45
+ try {
46
+ let freshPrompts = await convexClient.query("mcp_prompts:getMcpPrompts", {
47
+ apiKey: config.apiKey
48
+ });
49
+ if (config.selectedWorkspaces.length > 0) {
50
+ freshPrompts = freshPrompts.filter(
51
+ (p) => p.workspaceSlug && config.selectedWorkspaces.includes(p.workspaceSlug)
52
+ );
53
+ }
54
+ const freshPrompt = freshPrompts.find((p) => p.slug === prompt.slug && p.workspaceSlug === prompt.workspaceSlug);
55
+ if (freshPrompt) {
56
+ flattenedPrompt = freshPrompt.flattenedPrompt;
57
+ promptName = freshPrompt.name;
58
+ console.error(`[MCP] Fetched fresh data for: ${buildPromptName(prompt)}`);
59
+ } else {
60
+ console.error(
61
+ `[MCP] WARNING: Prompt "${prompt.slug}" not found in fresh fetch, using cached version`
62
+ );
63
+ }
64
+ } catch (error) {
65
+ console.error(
66
+ `[MCP] WARNING: Failed to fetch fresh data, using cached version: ${error instanceof Error ? error.message : "Unknown error"}`
67
+ );
68
+ }
69
+ if (!flattenedPrompt) {
70
+ throw new Error(
71
+ `Prompt "${promptName}" has no flattened content. Please re-save the prompt in ContextFS to regenerate it.`
72
+ );
73
+ }
74
+ console.error(`[MCP] Executed prompt: ${buildPromptName(prompt)}`);
75
+ return {
76
+ messages: [
77
+ {
78
+ role: "user",
79
+ content: {
80
+ type: "text",
81
+ text: flattenedPrompt
82
+ }
83
+ }
84
+ ]
85
+ };
86
+ };
87
+ }
88
+
89
+ // src/server.ts
90
+ var SYSTEM_TOOLS = [
91
+ // User-facing slash command: /ppm:system:optimize
92
+ {
93
+ name: "system:optimize",
94
+ description: "Evaluate a prompt and submit optimization suggestions to Prompt Project Manager",
95
+ inputSchema: {
96
+ type: "object",
97
+ properties: {}
98
+ },
99
+ isSlashCommand: true,
100
+ promptContent: `# Optimize
101
+
102
+ Evaluate the prompt you just used and submit optimization suggestions.
103
+
104
+ ## Instructions
105
+
106
+ 1. **Identify the prompt** from the PREVIOUS tool call in this conversation:
107
+ - Look at the last \`/ppm:*\` slash command or MCP tool you called
108
+ - The format is \`/ppm:workspace:prompt\` (e.g., \`/ppm:test:joke\` means workspace=\`test\`, prompt=\`joke\`)
109
+ - Extract the workspace slug (before the colon) and prompt slug (after the colon)
110
+ - **CRITICAL**: Use the EXACT slugs from the tool name, do NOT guess or invent slugs
111
+
112
+ 2. **Evaluate** for issues in these categories:
113
+ - **clarity**: Is the language clear and unambiguous?
114
+ - **specificity**: Are instructions specific enough?
115
+ - **structure**: Is the organization logical?
116
+ - **completeness**: Are there missing instructions?
117
+ - **efficiency**: Is there redundancy to remove?
118
+
119
+ 3. **Call the submission tool** (\`system:optimize-prompt\`) with:
120
+ - \`promptSlug\`: The prompt's slug
121
+ - \`workspaceSlug\`: The workspace slug
122
+ - \`suggestions\`: Array of issues found, each with category, priority, issue, originalText (optional), suggestedText
123
+
124
+ 4. The tool will submit and confirm the count
125
+
126
+ ## Scope of Analysis
127
+
128
+ **CRITICAL - READ CAREFULLY**:
129
+
130
+ The flattened prompt contains TWO parts:
131
+ 1. **MAIN PROMPT CONTENT** (at the top) - This is what you should analyze
132
+ 2. **Technical Reference Footer** (at the bottom) - **COMPLETELY IGNORE THIS ENTIRE SECTION**
133
+
134
+ **The Technical Reference Footer starts with \`# Technical Reference\` and includes:**
135
+ - \`## Variable Definitions:\` - Contains variable names, types, and descriptions
136
+ - \`## Snippet Definitions:\` - Contains snippet content
137
+ - \`## CRITICAL RULES:\` - System rules
138
+ - \`## Runtime Variables:\` - Runtime placeholders
139
+
140
+ **IGNORE ALL CONTENT IN THE TECHNICAL REFERENCE FOOTER** - even if it contains typos, grammar issues, or formatting problems. These sections are auto-generated from user-defined resources and should NOT be optimized through this tool.
141
+
142
+ **Only analyze and suggest changes for content BEFORE the \`# Technical Reference\` heading.**
143
+
144
+ ## User Feedback
145
+
146
+ If the user provided feedback about issues they experienced, it appears below:
147
+
148
+ **Feedback**: $ARGUMENTS
149
+
150
+ When feedback is provided:
151
+ - Treat it as context about real problems encountered during use
152
+ - Prioritize suggestions that address the described issues
153
+ - Look for patterns in the prompt that could cause the mentioned problem
154
+
155
+ ## Important
156
+
157
+ - Don't suggest changes to variable references like \`[VARIABLE_NAME]\` - those are intentional
158
+ - The user will review suggestions in Prompt Project Manager web UI`
159
+ },
160
+ // User-facing slash command: /ppm:system:feedback
161
+ {
162
+ name: "system:feedback",
163
+ description: "Submit user feedback about a prompt to create an optimization suggestion",
164
+ inputSchema: {
165
+ type: "object",
166
+ properties: {}
167
+ },
168
+ isSlashCommand: true,
169
+ promptContent: `# Feedback
170
+
171
+ Submit your feedback about a prompt to create an optimization suggestion.
172
+
173
+ ## Instructions
174
+
175
+ 1. **Identify the prompt** from the PREVIOUS tool call in this conversation:
176
+ - Look at the last \`/ppm:*\` slash command or MCP tool you called
177
+ - The format is \`/ppm:workspace:prompt\` (e.g., \`/ppm:test:joke\` means workspace=\`test\`, prompt=\`joke\`)
178
+ - Extract the workspace slug (before the colon) and prompt slug (after the colon)
179
+ - **CRITICAL**: Use the EXACT slugs from the tool name, do NOT guess or invent slugs
180
+
181
+ 2. **Read the user's feedback** from $ARGUMENTS below
182
+
183
+ 3. **Structure the feedback** into a proper optimization suggestion:
184
+ - Determine the most appropriate category: clarity, specificity, structure, completeness, or efficiency
185
+ - Assess priority: "high" (critical issue), "medium" (noticeable problem), "low" (minor improvement)
186
+ - Identify the specific text to change (originalText) if applicable
187
+ - Write a clear, actionable fix (suggestedText)
188
+
189
+ 4. **Call the submission tool** (\`system:optimize-prompt\`) with:
190
+ - \`promptSlug\`: The prompt's slug
191
+ - \`workspaceSlug\`: The workspace slug
192
+ - \`suggestions\`: Array containing a SINGLE suggestion based on the user's feedback
193
+
194
+ ## User Feedback
195
+
196
+ **$ARGUMENTS**
197
+
198
+ ## Scope of Analysis
199
+
200
+ When analyzing the prompt to address the feedback:
201
+ - **ONLY** focus on content BEFORE the \`# Technical Reference\` heading
202
+ - **IGNORE** the Technical Reference Footer (Variable Definitions, Snippet Definitions, CRITICAL RULES, Runtime Variables)
203
+ - Don't suggest changes to variable references like \`[VARIABLE_NAME]\` - those are intentional
204
+
205
+ ## Example
206
+
207
+ If the user runs:
208
+ \`/feedback the output format should specify JSON not markdown\`
209
+
210
+ You should:
211
+ 1. Find the prompt that was used earlier in the session
212
+ 2. Create a suggestion like:
213
+ \`\`\`json
214
+ {
215
+ "category": "specificity",
216
+ "priority": "medium",
217
+ "issue": "Output format should specify JSON instead of markdown",
218
+ "originalText": "Output format: markdown",
219
+ "suggestedText": "Output format: JSON with fields: { result: string, status: string }"
220
+ }
221
+ \`\`\`
222
+ 3. Submit via \`system:optimize-prompt\` tool
223
+
224
+ ## Important
225
+
226
+ - Create exactly ONE suggestion from the user's feedback (not multiple)
227
+ - The suggestion will be reviewed in Prompt Project Manager web UI
228
+ - Use your understanding of the session to interpret vague feedback into actionable fixes`
229
+ },
230
+ // Utility tool: /ppm:system:prompts
231
+ {
232
+ name: "system:prompts",
233
+ description: "List all available prompts from Prompt Project Manager",
234
+ inputSchema: {
235
+ type: "object",
236
+ properties: {
237
+ search: {
238
+ type: "string",
239
+ description: "Optional search term to filter prompts by name or description (case-insensitive)"
240
+ }
241
+ }
242
+ },
243
+ isSlashCommand: true,
244
+ promptContent: `# List Prompts
245
+
246
+ This is a utility tool. Call it directly as a tool to list all available prompts.
247
+
248
+ Example: Use the \`system:prompts\` tool with optional \`search\` parameter to filter results.`
249
+ },
250
+ // Backend submission tool (called by system:optimize and system:feedback)
251
+ // NOT a slash command - only available as a tool for AI to call
252
+ {
253
+ name: "system:optimize-prompt",
254
+ description: "Submit optimization suggestions to Prompt Project Manager (internal tool)",
255
+ inputSchema: {
256
+ type: "object",
257
+ properties: {
258
+ promptSlug: {
259
+ type: "string",
260
+ description: "The slug of the prompt to optimize (e.g., 'code-review')"
261
+ },
262
+ workspaceSlug: {
263
+ type: "string",
264
+ description: "The workspace containing the prompt (e.g., 'personal')"
265
+ },
266
+ suggestions: {
267
+ type: "array",
268
+ description: "Array of optimization suggestions",
269
+ items: {
270
+ type: "object",
271
+ properties: {
272
+ category: {
273
+ type: "string",
274
+ description: "Category: 'clarity', 'specificity', 'structure', 'completeness', 'efficiency'"
275
+ },
276
+ priority: {
277
+ type: "string",
278
+ enum: ["high", "medium", "low"],
279
+ description: "Priority level"
280
+ },
281
+ issue: {
282
+ type: "string",
283
+ description: "Brief description of the problem"
284
+ },
285
+ originalText: {
286
+ type: "string",
287
+ description: "Optional: The specific text that should be changed"
288
+ },
289
+ suggestedText: {
290
+ type: "string",
291
+ description: "The improved text"
292
+ }
293
+ },
294
+ required: ["category", "priority", "issue", "suggestedText"]
295
+ }
296
+ }
297
+ },
298
+ required: ["promptSlug", "workspaceSlug", "suggestions"]
299
+ },
300
+ isSlashCommand: false,
301
+ // Tool only - not a slash command
302
+ promptContent: `# Submit Optimization Suggestions
303
+
304
+ This is an internal submission tool. Do not call it directly.
305
+
306
+ Use \`/ppm:system:optimize\` for full prompt evaluation or \`/ppm:system:feedback\` to submit specific feedback.`
307
+ }
308
+ ];
309
+ async function startServer(config, convexClient) {
310
+ console.error("[MCP] Validating API key...");
311
+ const validation = await validateApiKey(convexClient, config.apiKey);
312
+ if (!validation.valid) {
313
+ throw new Error(`Invalid API key: ${validation.error}`);
314
+ }
315
+ console.error(`[MCP] API key validated for user: ${validation.userId}`);
316
+ console.error("[MCP] Fetching exposed prompts...");
317
+ const prompts = await fetchMcpPrompts(convexClient, config.apiKey, config.selectedWorkspaces);
318
+ console.error(`[MCP] Found ${prompts.length} exposed prompts`);
319
+ if (prompts.length === 0) {
320
+ if (config.selectedWorkspaces.length > 0) {
321
+ console.error(
322
+ `[MCP] WARNING: No prompts found in workspaces: ${config.selectedWorkspaces.join(", ")}. Check that these workspace slugs exist and contain prompts.`
323
+ );
324
+ } else {
325
+ console.error(
326
+ "[MCP] WARNING: No prompts found. Create some prompts in Prompt Project Manager to expose them via MCP."
327
+ );
328
+ }
329
+ }
330
+ const validPrompts = prompts.filter((p) => {
331
+ if (!p.flattenedPrompt) {
332
+ console.error(
333
+ `[MCP] WARNING: Skipping prompt "${p.name}" (${p.slug}) - missing flattened content. Re-save in ContextFS to regenerate.`
334
+ );
335
+ return false;
336
+ }
337
+ return true;
338
+ });
339
+ if (config.selectedWorkspaces.length > 0) {
340
+ console.error(`[MCP] Workspace filter: ${config.selectedWorkspaces.join(", ")}`);
341
+ } else {
342
+ console.error(`[MCP] Workspace filter: ALL (no --workspaces specified)`);
343
+ }
344
+ console.error(`[MCP] Registering ${validPrompts.length} prompts...`);
345
+ const workspaceSlugs = new Set(validPrompts.map((p) => p.workspaceSlug).filter((s) => !!s));
346
+ const dynamicTicketTools = [];
347
+ for (const workspaceSlug of workspaceSlugs) {
348
+ dynamicTicketTools.push({
349
+ name: `${workspaceSlug}:tickets:list`,
350
+ description: `List pending tickets in the "${workspaceSlug}" workspace queue`,
351
+ workspaceSlug,
352
+ type: "list"
353
+ });
354
+ dynamicTicketTools.push({
355
+ name: `${workspaceSlug}:tickets:next`,
356
+ description: `Execute and remove the next ticket from the "${workspaceSlug}" workspace queue. Optionally specify a ticket number or slug to execute a specific ticket.`,
357
+ workspaceSlug,
358
+ type: "next"
359
+ });
360
+ dynamicTicketTools.push({
361
+ name: `${workspaceSlug}:tickets:get`,
362
+ description: `Get a specific ticket from the "${workspaceSlug}" workspace by number or slug`,
363
+ workspaceSlug,
364
+ type: "get"
365
+ });
366
+ dynamicTicketTools.push({
367
+ name: `${workspaceSlug}:tickets:create`,
368
+ description: `Create a new ticket in the "${workspaceSlug}" workspace queue`,
369
+ workspaceSlug,
370
+ type: "create"
371
+ });
372
+ }
373
+ console.error(`[MCP] Registering ${dynamicTicketTools.length} ticket tools for ${workspaceSlugs.size} workspace(s)...`);
374
+ const promptNames = /* @__PURE__ */ new Set();
375
+ const duplicates = [];
376
+ validPrompts.forEach((p) => {
377
+ const name = buildPromptName(p);
378
+ if (promptNames.has(name)) {
379
+ duplicates.push(name);
380
+ }
381
+ promptNames.add(name);
382
+ });
383
+ if (duplicates.length > 0) {
384
+ console.error(
385
+ `[MCP] WARNING: Duplicate prompt names detected: ${duplicates.join(", ")}. Only the first occurrence will be registered.`
386
+ );
387
+ }
388
+ const server = new Server(
389
+ {
390
+ name: "ppm-mcp-server",
391
+ version: "2.8.1"
392
+ },
393
+ {
394
+ capabilities: {
395
+ prompts: {},
396
+ tools: {}
397
+ }
398
+ }
399
+ );
400
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
401
+ const uniquePrompts = Array.from(
402
+ new Map(validPrompts.map((p) => [buildPromptName(p), p])).values()
403
+ );
404
+ const systemPromptSchemas = SYSTEM_TOOLS.filter((st) => st.isSlashCommand).map((st) => ({
405
+ name: st.name,
406
+ description: st.description
407
+ }));
408
+ const ticketPromptSchemas = dynamicTicketTools.filter((tt) => tt.type !== "get").map((tt) => ({
409
+ name: tt.name,
410
+ description: tt.description
411
+ }));
412
+ return {
413
+ prompts: [
414
+ ...systemPromptSchemas,
415
+ ...ticketPromptSchemas,
416
+ ...uniquePrompts.map(buildPromptSchema)
417
+ ]
418
+ };
419
+ });
420
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
421
+ const promptName = request.params.name;
422
+ const systemTool = SYSTEM_TOOLS.find((st) => st.name === promptName);
423
+ if (systemTool) {
424
+ return {
425
+ description: systemTool.description,
426
+ messages: [
427
+ {
428
+ role: "user",
429
+ content: {
430
+ type: "text",
431
+ text: systemTool.promptContent
432
+ }
433
+ }
434
+ ]
435
+ };
436
+ }
437
+ const ticketTool = dynamicTicketTools.find((tt) => tt.name === promptName);
438
+ const ticketNextMatch = promptName.match(/^(.+):tickets:next\s+(.+)$/);
439
+ if (ticketTool || ticketNextMatch) {
440
+ let promptContent;
441
+ let description;
442
+ if (ticketNextMatch) {
443
+ const workspaceSlug = ticketNextMatch[1];
444
+ const ticketArg = ticketNextMatch[2].trim();
445
+ description = `Execute ticket "${ticketArg}" from "${workspaceSlug}"`;
446
+ promptContent = `Execute ticket "${ticketArg}" from the "${workspaceSlug}" workspace queue.
447
+
448
+ Call the \`${workspaceSlug}:tickets:next\` tool with ticketSlug: "${ticketArg}" to get and close the specified ticket.`;
449
+ } else if (ticketTool.type === "list") {
450
+ description = ticketTool.description;
451
+ promptContent = `List all pending tickets in the "${ticketTool.workspaceSlug}" workspace queue.
452
+
453
+ Call the \`${ticketTool.name}\` tool to get the list.`;
454
+ } else if (ticketTool.type === "next") {
455
+ description = ticketTool.description;
456
+ promptContent = `Get and execute the next ticket from the "${ticketTool.workspaceSlug}" workspace queue.
457
+
458
+ Call the \`${ticketTool.name}\` tool to get the next ticket. This will atomically close the ticket and return its content.
459
+
460
+ You can also specify a ticket: /ppm:${ticketTool.workspaceSlug}:tickets:next <number-or-slug>`;
461
+ } else if (ticketTool.type === "create") {
462
+ description = ticketTool.description;
463
+ promptContent = `Create a new ticket in the "${ticketTool.workspaceSlug}" workspace queue.
464
+
465
+ ## How This Works
466
+ The user provides **instructions** (not raw content). You interpret those instructions to generate the ticket.
467
+
468
+ ## Instructions
469
+ 1. **Read the user's request** - they may reference a file, ask you to summarize a session, or describe what they want
470
+ 2. **Process the input** - read files, extract relevant sections, summarize as needed
471
+ 3. **Generate ticket content** with a clear, descriptive title as the first line
472
+ 4. **Call the tool** with the generated content
473
+
474
+ ## Content Format
475
+ \`\`\`
476
+ [Clear descriptive title - this becomes the slug]
477
+
478
+ [Body content - tasks, description, context, etc.]
479
+ \`\`\`
480
+
481
+ ## Examples
482
+ - "create a ticket from /path/to/plan.md" \u2192 Read file, use as ticket content
483
+ - "summarize our brainstorm into a ticket" \u2192 Extract key points from conversation
484
+ - "create a ticket for the auth bug we discussed" \u2192 Generate from session context
485
+ - "just the tasks from this file" \u2192 Extract only task items
486
+
487
+ Call the \`${ticketTool.name}\` tool with the final generated content.`;
488
+ } else {
489
+ description = ticketTool.description;
490
+ promptContent = `This command is not available as a slash command. Use the \`${ticketTool.name}\` tool directly to inspect a ticket.`;
491
+ }
492
+ return {
493
+ description,
494
+ messages: [
495
+ {
496
+ role: "user",
497
+ content: {
498
+ type: "text",
499
+ text: promptContent
500
+ }
501
+ }
502
+ ]
503
+ };
504
+ }
505
+ const prompt = validPrompts.find((p) => buildPromptName(p) === promptName);
506
+ if (!prompt) {
507
+ throw new Error(`Unknown prompt: ${promptName}`);
508
+ }
509
+ const handler = buildPromptHandler(prompt, config, convexClient);
510
+ return await handler();
511
+ });
512
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
513
+ const uniquePrompts = Array.from(
514
+ new Map(validPrompts.map((p) => [buildPromptName(p), p])).values()
515
+ );
516
+ const tools = [
517
+ // System tools with full input schemas
518
+ ...SYSTEM_TOOLS.map((st) => ({
519
+ name: st.name,
520
+ description: st.description,
521
+ inputSchema: st.inputSchema
522
+ })),
523
+ // Dynamic ticket tools per workspace (Ticket 135, 149)
524
+ ...dynamicTicketTools.map((tt) => ({
525
+ name: tt.name,
526
+ description: tt.description,
527
+ inputSchema: tt.type === "get" ? {
528
+ type: "object",
529
+ properties: {
530
+ ticketSlug: {
531
+ type: "string",
532
+ description: "Ticket number (e.g., '102') or full slug (e.g., '102-fix-auth')"
533
+ }
534
+ },
535
+ required: ["ticketSlug"]
536
+ } : tt.type === "next" ? {
537
+ type: "object",
538
+ properties: {
539
+ ticketSlug: {
540
+ type: "string",
541
+ description: "Optional: Ticket number (e.g., '102') or full slug (e.g., '102-fix-auth'). If not provided, executes the next ticket in queue."
542
+ }
543
+ },
544
+ required: []
545
+ } : tt.type === "create" ? {
546
+ type: "object",
547
+ properties: {
548
+ content: {
549
+ type: "string",
550
+ description: "The generated ticket content. First line should be a clear descriptive title (becomes the slug). Body contains tasks, description, context as needed."
551
+ }
552
+ },
553
+ required: ["content"]
554
+ } : {
555
+ type: "object",
556
+ properties: {},
557
+ required: []
558
+ }
559
+ })),
560
+ // Each user prompt becomes a discoverable tool
561
+ ...uniquePrompts.map((prompt) => ({
562
+ name: buildPromptName(prompt),
563
+ description: prompt.description || `Prompt: ${prompt.name}`,
564
+ inputSchema: {
565
+ type: "object",
566
+ properties: {},
567
+ required: []
568
+ }
569
+ }))
570
+ ];
571
+ return { tools };
572
+ });
573
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
574
+ const toolName = request.params.name;
575
+ if (toolName === "system:prompts") {
576
+ const searchTerm = request.params.arguments?.search;
577
+ const uniquePrompts = Array.from(
578
+ new Map(validPrompts.map((p) => [buildPromptName(p), p])).values()
579
+ );
580
+ let filteredSystemTools = SYSTEM_TOOLS;
581
+ if (searchTerm) {
582
+ const lowerSearch = searchTerm.toLowerCase();
583
+ filteredSystemTools = SYSTEM_TOOLS.filter(
584
+ (st) => st.name.toLowerCase().includes(lowerSearch) || st.description.toLowerCase().includes(lowerSearch)
585
+ );
586
+ }
587
+ let filteredPrompts = uniquePrompts;
588
+ if (searchTerm) {
589
+ const lowerSearch = searchTerm.toLowerCase();
590
+ filteredPrompts = uniquePrompts.filter(
591
+ (p) => buildPromptName(p).toLowerCase().includes(lowerSearch) || p.name.toLowerCase().includes(lowerSearch) || p.description?.toLowerCase().includes(lowerSearch)
592
+ );
593
+ }
594
+ filteredPrompts.sort((a, b) => {
595
+ const aName = buildPromptName(a);
596
+ const bName = buildPromptName(b);
597
+ return aName.localeCompare(bName);
598
+ });
599
+ const systemToolList = filteredSystemTools.map((st) => `\u2022 ${st.name}
600
+ Type: System
601
+ Description: ${st.description}`).join("\n\n");
602
+ const userPromptList = filteredPrompts.map((p) => {
603
+ const name = buildPromptName(p);
604
+ const workspace = p.workspaceSlug || "unknown";
605
+ const desc = p.description || "No description";
606
+ return `\u2022 ${name}
607
+ Workspace: ${workspace}
608
+ Description: ${desc}`;
609
+ }).join("\n\n");
610
+ const totalCount = filteredSystemTools.length + filteredPrompts.length;
611
+ const summary = searchTerm ? `Found ${totalCount} prompts matching "${searchTerm}":` : `Available prompts (${totalCount} total):`;
612
+ const sections = [];
613
+ if (systemToolList) {
614
+ sections.push(`**System Tools:**
615
+
616
+ ${systemToolList}`);
617
+ }
618
+ if (userPromptList) {
619
+ sections.push(`**User Prompts:**
620
+
621
+ ${userPromptList}`);
622
+ }
623
+ return {
624
+ content: [
625
+ {
626
+ type: "text",
627
+ text: `${summary}
628
+
629
+ ${sections.join("\n\n") || "No prompts found."}`
630
+ }
631
+ ]
632
+ };
633
+ }
634
+ if (toolName === "system:optimize-prompt") {
635
+ const input = request.params.arguments;
636
+ if (!input.promptSlug || !input.workspaceSlug || !input.suggestions) {
637
+ return {
638
+ content: [
639
+ {
640
+ type: "text",
641
+ text: "Error: Missing required fields. Please provide promptSlug, workspaceSlug, and suggestions."
642
+ }
643
+ ],
644
+ isError: true
645
+ };
646
+ }
647
+ if (!Array.isArray(input.suggestions) || input.suggestions.length === 0) {
648
+ return {
649
+ content: [
650
+ {
651
+ type: "text",
652
+ text: "Error: suggestions must be a non-empty array."
653
+ }
654
+ ],
655
+ isError: true
656
+ };
657
+ }
658
+ try {
659
+ const result2 = await convexClient.mutation(
660
+ "prompts:addOptimizationSuggestions",
661
+ {
662
+ apiKey: config.apiKey,
663
+ workspaceSlug: input.workspaceSlug,
664
+ promptSlug: input.promptSlug,
665
+ suggestions: input.suggestions
666
+ }
667
+ );
668
+ return {
669
+ content: [
670
+ {
671
+ type: "text",
672
+ text: `Submitted ${result2.addedCount} optimization suggestion(s) for prompt "${input.promptSlug}" in workspace "${input.workspaceSlug}". Review and apply them in Prompt Project Manager.`
673
+ }
674
+ ]
675
+ };
676
+ } catch (error) {
677
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
678
+ console.error(`[MCP] system:optimize-prompt error:`, error);
679
+ return {
680
+ content: [
681
+ {
682
+ type: "text",
683
+ text: `Error submitting optimization suggestions: ${errorMessage}`
684
+ }
685
+ ],
686
+ isError: true
687
+ };
688
+ }
689
+ }
690
+ const ticketTool = dynamicTicketTools.find((tt) => tt.name === toolName);
691
+ if (ticketTool) {
692
+ if (ticketTool.type === "list") {
693
+ try {
694
+ const tickets = await convexClient.query(
695
+ "mcp_tickets:getMcpTickets",
696
+ {
697
+ apiKey: config.apiKey,
698
+ workspaceSlug: ticketTool.workspaceSlug
699
+ }
700
+ );
701
+ if (tickets.length === 0) {
702
+ return {
703
+ content: [
704
+ {
705
+ type: "text",
706
+ text: `No pending tickets in workspace "${ticketTool.workspaceSlug}".`
707
+ }
708
+ ]
709
+ };
710
+ }
711
+ const ticketList = tickets.map((t) => `${t.position}. [${t.slug}] ${t.preview}`).join("\n");
712
+ return {
713
+ content: [
714
+ {
715
+ type: "text",
716
+ text: `Pending tickets in "${ticketTool.workspaceSlug}" (${tickets.length} total):
717
+
718
+ ${ticketList}`
719
+ }
720
+ ]
721
+ };
722
+ } catch (error) {
723
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
724
+ console.error(`[MCP] tickets:list error:`, error);
725
+ return {
726
+ content: [
727
+ {
728
+ type: "text",
729
+ text: `Error listing tickets: ${errorMessage}`
730
+ }
731
+ ],
732
+ isError: true
733
+ };
734
+ }
735
+ } else if (ticketTool.type === "next") {
736
+ const ticketSlug = request.params.arguments?.ticketSlug;
737
+ try {
738
+ const result2 = await convexClient.mutation(
739
+ "mcp_tickets:executeMcpNextTicket",
740
+ {
741
+ apiKey: config.apiKey,
742
+ workspaceSlug: ticketTool.workspaceSlug,
743
+ ticketSlug
744
+ // Optional: specific ticket to execute
745
+ }
746
+ );
747
+ if (!result2) {
748
+ const message = ticketSlug ? `Ticket "${ticketSlug}" not found or already closed in workspace "${ticketTool.workspaceSlug}".` : `No pending tickets in workspace "${ticketTool.workspaceSlug}".`;
749
+ return {
750
+ content: [
751
+ {
752
+ type: "text",
753
+ text: message
754
+ }
755
+ ]
756
+ };
757
+ }
758
+ return {
759
+ content: [
760
+ {
761
+ type: "text",
762
+ text: `# Ticket: ${result2.slug}
763
+
764
+ ${result2.content}
765
+
766
+ ---
767
+ _Ticket closed. ${result2.remainingTickets} ticket(s) remaining in queue._`
768
+ }
769
+ ]
770
+ };
771
+ } catch (error) {
772
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
773
+ console.error(`[MCP] tickets:next error:`, error);
774
+ return {
775
+ content: [
776
+ {
777
+ type: "text",
778
+ text: `Error executing ticket: ${errorMessage}`
779
+ }
780
+ ],
781
+ isError: true
782
+ };
783
+ }
784
+ } else if (ticketTool.type === "create") {
785
+ const content = request.params.arguments?.content;
786
+ if (!content) {
787
+ return {
788
+ content: [
789
+ {
790
+ type: "text",
791
+ text: `Error: Missing content parameter. Provide the ticket content (first line becomes the slug).`
792
+ }
793
+ ],
794
+ isError: true
795
+ };
796
+ }
797
+ try {
798
+ const result2 = await convexClient.mutation(
799
+ "mcp_tickets:createMcpTicket",
800
+ {
801
+ apiKey: config.apiKey,
802
+ workspaceSlug: ticketTool.workspaceSlug,
803
+ content
804
+ }
805
+ );
806
+ return {
807
+ content: [
808
+ {
809
+ type: "text",
810
+ text: `\u2705 Created ticket [${result2.slug}] in workspace "${ticketTool.workspaceSlug}".
811
+
812
+ Position: #${result2.position} of ${result2.totalOpen} open tickets
813
+ Preview: ${result2.preview}`
814
+ }
815
+ ]
816
+ };
817
+ } catch (error) {
818
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
819
+ console.error(`[MCP] tickets:create error:`, error);
820
+ return {
821
+ content: [
822
+ {
823
+ type: "text",
824
+ text: `Error creating ticket: ${errorMessage}`
825
+ }
826
+ ],
827
+ isError: true
828
+ };
829
+ }
830
+ } else {
831
+ const ticketSlug = request.params.arguments?.ticketSlug;
832
+ if (!ticketSlug) {
833
+ return {
834
+ content: [
835
+ {
836
+ type: "text",
837
+ text: `Error: Missing ticketSlug parameter. Usage: Provide a ticket number (e.g., "102") or full slug (e.g., "102-fix-auth").`
838
+ }
839
+ ],
840
+ isError: true
841
+ };
842
+ }
843
+ try {
844
+ const result2 = await convexClient.query(
845
+ "mcp_tickets:getMcpTicket",
846
+ {
847
+ apiKey: config.apiKey,
848
+ workspaceSlug: ticketTool.workspaceSlug,
849
+ ticketSlug
850
+ }
851
+ );
852
+ if (!result2) {
853
+ return {
854
+ content: [
855
+ {
856
+ type: "text",
857
+ text: `Ticket "${ticketSlug}" not found in workspace "${ticketTool.workspaceSlug}".`
858
+ }
859
+ ]
860
+ };
861
+ }
862
+ const statusLabel = result2.status === "open" ? "\u{1F7E2} OPEN" : "\u26AB CLOSED";
863
+ const closedInfo = result2.closedAt ? `
864
+ Closed: ${new Date(result2.closedAt).toISOString()}` : "";
865
+ return {
866
+ content: [
867
+ {
868
+ type: "text",
869
+ text: `# Ticket: ${result2.slug}
870
+
871
+ Status: ${statusLabel}${closedInfo}
872
+
873
+ ---
874
+
875
+ ${result2.content}`
876
+ }
877
+ ]
878
+ };
879
+ } catch (error) {
880
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
881
+ console.error(`[MCP] tickets:get error:`, error);
882
+ return {
883
+ content: [
884
+ {
885
+ type: "text",
886
+ text: `Error getting ticket: ${errorMessage}`
887
+ }
888
+ ],
889
+ isError: true
890
+ };
891
+ }
892
+ }
893
+ }
894
+ const prompt = validPrompts.find((p) => buildPromptName(p) === toolName);
895
+ if (!prompt) {
896
+ throw new Error(`Unknown tool: ${toolName}`);
897
+ }
898
+ const handler = buildPromptHandler(prompt, config, convexClient);
899
+ const result = await handler();
900
+ const promptText = result.messages.map((msg) => typeof msg.content === "string" ? msg.content : msg.content.text).join("\n\n");
901
+ return {
902
+ content: [
903
+ {
904
+ type: "text",
905
+ text: promptText
906
+ }
907
+ ]
908
+ };
909
+ });
910
+ const transport = new StdioServerTransport();
911
+ await server.connect(transport);
912
+ console.error("[MCP] Server started successfully");
913
+ console.error(`[MCP] Deployment: ${config.isDev ? "DEVELOPMENT" : "PRODUCTION"}`);
914
+ console.error(`[MCP] Convex URL: ${config.convexUrl}`);
915
+ console.error(`[MCP] Data mode: REAL-TIME (fetches fresh data on each invocation)`);
916
+ const allPromptNames = [...Array.from(promptNames)].sort();
917
+ console.error(`[MCP] Prompts available: ${allPromptNames.join(", ")}`);
918
+ console.error(`[MCP] - Total prompts: ${promptNames.size}`);
919
+ setInterval(() => {
920
+ }, 6e4);
921
+ return new Promise(() => {
922
+ });
923
+ }
924
+ async function validateApiKey(client, apiKey) {
925
+ try {
926
+ const result = await client.query("apiKeys:validateApiKey", { key: apiKey });
927
+ if (result) {
928
+ return { valid: true, userId: result.userId };
929
+ }
930
+ return { valid: false, error: "Invalid API key" };
931
+ } catch (error) {
932
+ return {
933
+ valid: false,
934
+ error: error instanceof Error ? error.message : "Unknown error"
935
+ };
936
+ }
937
+ }
938
+ async function fetchMcpPrompts(client, apiKey, workspaces) {
939
+ try {
940
+ const prompts = await client.query("mcp_prompts:getMcpPrompts", {
941
+ apiKey,
942
+ workspaces: workspaces.length > 0 ? workspaces : void 0
943
+ // Only pass if specified
944
+ });
945
+ return prompts;
946
+ } catch (error) {
947
+ throw new Error(
948
+ `Failed to fetch prompts: ${error instanceof Error ? error.message : "Unknown error"}`
949
+ );
950
+ }
951
+ }
952
+
953
+ // src/index.ts
954
+ async function main() {
955
+ try {
956
+ const argv = minimist(process.argv.slice(2));
957
+ const isDev = argv.dev === true;
958
+ const workspacesArg = argv.workspaces || argv.w;
959
+ let selectedWorkspaces = [];
960
+ if (workspacesArg) {
961
+ if (Array.isArray(workspacesArg)) {
962
+ selectedWorkspaces = workspacesArg.flatMap((w) => String(w).split(","));
963
+ } else {
964
+ selectedWorkspaces = String(workspacesArg).split(",");
965
+ }
966
+ selectedWorkspaces = selectedWorkspaces.map((s) => s.trim()).filter(Boolean);
967
+ }
968
+ const apiKey = process.env.PPM_API_KEY || process.env.THEPROMPTEDITOR_API_KEY;
969
+ if (!apiKey) {
970
+ console.error(
971
+ "[MCP] ERROR: Missing PPM_API_KEY environment variable"
972
+ );
973
+ console.error(
974
+ "[MCP] Please set your API key from Prompt Project Manager Settings page:"
975
+ );
976
+ console.error("[MCP] export PPM_API_KEY=your_api_key_here");
977
+ process.exit(1);
978
+ }
979
+ const convexClient = createConvexClient(isDev);
980
+ const convexUrl = isDev ? "https://hallowed-shrimp-344.convex.cloud" : "https://trustworthy-squirrel-735.convex.cloud";
981
+ const config = {
982
+ apiKey,
983
+ isDev,
984
+ convexUrl,
985
+ selectedWorkspaces
986
+ // Workspace slugs to filter (empty = all workspaces)
987
+ };
988
+ await startServer(config, convexClient);
989
+ console.error("[MCP] WARNING: startServer promise resolved unexpectedly!");
990
+ } catch (error) {
991
+ console.error(
992
+ "[MCP] FATAL ERROR:",
993
+ error instanceof Error ? error.message : "Unknown error"
994
+ );
995
+ if (error instanceof Error && error.stack) {
996
+ console.error(error.stack);
997
+ }
998
+ process.exit(1);
999
+ }
1000
+ }
1001
+ process.on("SIGINT", () => {
1002
+ console.error("[MCP] Received SIGINT, shutting down gracefully...");
1003
+ process.exit(0);
1004
+ });
1005
+ process.on("SIGTERM", () => {
1006
+ console.error("[MCP] Received SIGTERM, shutting down gracefully...");
1007
+ process.exit(0);
1008
+ });
1009
+ main();
1010
+ //# sourceMappingURL=index.js.map