@promptprojectmanager/mcp-server 4.9.4 → 4.9.6

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 CHANGED
@@ -22,6 +22,313 @@ import {
22
22
  ListToolsRequestSchema
23
23
  } from "@modelcontextprotocol/sdk/types.js";
24
24
 
25
+ // src/auth/agent-resolver.ts
26
+ function buildAuthArgs(config) {
27
+ return { projectToken: config.projectToken };
28
+ }
29
+ function resolveAgent(paramAgent) {
30
+ if (paramAgent) return paramAgent;
31
+ const envAgent = process.env.PPM_AGENT_NAME;
32
+ if (envAgent) return envAgent;
33
+ return void 0;
34
+ }
35
+
36
+ // src/auth/token-validator.ts
37
+ async function validateProjectToken(client, token) {
38
+ try {
39
+ const typedClient = client;
40
+ const result = await typedClient.query(
41
+ "projects:validateProjectToken",
42
+ { token }
43
+ );
44
+ if (result && result.valid) {
45
+ return {
46
+ valid: true,
47
+ projectId: result.projectId,
48
+ projectSlug: result.projectSlug,
49
+ ownerId: result.ownerId
50
+ };
51
+ }
52
+ return { valid: false, error: "Invalid project token" };
53
+ } catch (error) {
54
+ return {
55
+ valid: false,
56
+ error: error instanceof Error ? error.message : "Unknown error"
57
+ };
58
+ }
59
+ }
60
+
61
+ // src/tools/registry.ts
62
+ function getTicketToolDefinitions(projectSlug) {
63
+ return [
64
+ {
65
+ name: "tickets_work",
66
+ type: "work",
67
+ description: `Get work from the "${projectSlug}" project. With no args: gets next ticket from open queue. With ticket slug/number: opens or resumes that specific ticket.`,
68
+ inputSchema: {
69
+ type: "object",
70
+ properties: {
71
+ ticketSlug: {
72
+ type: "string",
73
+ description: "Optional: Ticket number (e.g., '102') or full slug. If not provided, gets next ticket from open queue."
74
+ },
75
+ agent: {
76
+ type: "string",
77
+ description: "Optional: Agent slug to filter queue. Overrides PPM_AGENT_NAME env var. Omit for owner mode (no filtering)."
78
+ }
79
+ },
80
+ required: []
81
+ }
82
+ },
83
+ {
84
+ name: "tickets_close",
85
+ type: "close",
86
+ description: `Mark a working ticket as completed in the "${projectSlug}" project. If a hook is configured, first call returns instructions - then call again with metadata.`,
87
+ inputSchema: {
88
+ type: "object",
89
+ properties: {
90
+ ticketSlug: {
91
+ type: "string",
92
+ description: "Ticket number (e.g., '102') or full slug (e.g., '102-fix-auth')"
93
+ },
94
+ metadata: {
95
+ type: "object",
96
+ description: "Optional: Key-value metadata to store with the closed ticket (e.g., evaluation scores, completion notes).",
97
+ additionalProperties: {
98
+ oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }]
99
+ }
100
+ }
101
+ },
102
+ required: ["ticketSlug"]
103
+ }
104
+ },
105
+ {
106
+ name: "tickets_pause",
107
+ type: "pause",
108
+ description: `Pause a working ticket and return to queue. Before calling, review the full session context and generate comprehensive checkpoint content that enables seamless handoff to another agent. Include: completed tasks with specifics, key decisions and rationale, sticking points and resolutions, user preferences discovered, remaining work with file:line references, and open questions.`,
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ ticketSlug: {
113
+ type: "string",
114
+ description: "Ticket number (e.g., '102') or full slug (e.g., '102-fix-auth')"
115
+ },
116
+ content: {
117
+ type: "string",
118
+ description: "Comprehensive checkpoint for handoff. Review the session and include: ## Progress (completed tasks with specifics, decisions made and why) ## Context (user preferences, edge cases discovered, key feedback received) ## Remaining (specific next steps, file:line references if applicable) ## Blockers (unresolved issues, open questions). This enables the next agent to continue seamlessly."
119
+ }
120
+ },
121
+ required: ["ticketSlug", "content"]
122
+ }
123
+ },
124
+ {
125
+ name: "tickets_create",
126
+ type: "create",
127
+ description: `Create a new ticket in the "${projectSlug}" project queue`,
128
+ inputSchema: {
129
+ type: "object",
130
+ properties: {
131
+ content: {
132
+ type: "string",
133
+ description: "The generated ticket content. First line should be a clear descriptive title (becomes the slug). Body contains tasks, description, context as needed."
134
+ },
135
+ agent: {
136
+ type: "string",
137
+ description: "Optional: Agent slug to assign ticket to. Overrides PPM_AGENT_NAME env var. Omit for unassigned."
138
+ }
139
+ },
140
+ required: ["content"]
141
+ }
142
+ },
143
+ {
144
+ name: "tickets_search",
145
+ type: "search",
146
+ description: `Search for tickets by content in the "${projectSlug}" project`,
147
+ inputSchema: {
148
+ type: "object",
149
+ properties: {
150
+ query: {
151
+ type: "string",
152
+ description: "Search query (min 3 characters)"
153
+ },
154
+ agent: {
155
+ type: "string",
156
+ description: "Optional: Agent slug to filter results. Overrides PPM_AGENT_NAME env var. Omit for all tickets."
157
+ }
158
+ },
159
+ required: ["query"]
160
+ }
161
+ },
162
+ {
163
+ name: "tickets_get",
164
+ type: "get",
165
+ description: `Get a specific ticket by number or slug from "${projectSlug}" (read-only)`,
166
+ inputSchema: {
167
+ type: "object",
168
+ properties: {
169
+ ticketSlug: {
170
+ type: "string",
171
+ description: "Ticket number (e.g., '102') or full slug"
172
+ }
173
+ },
174
+ required: ["ticketSlug"]
175
+ }
176
+ },
177
+ {
178
+ name: "tickets_list",
179
+ type: "list",
180
+ description: `List active tickets in the "${projectSlug}" project (backlog + open + working)`,
181
+ inputSchema: {
182
+ type: "object",
183
+ properties: {
184
+ agent: {
185
+ type: "string",
186
+ description: "Optional: Agent slug to filter tickets. Overrides PPM_AGENT_NAME env var. Omit for all tickets."
187
+ }
188
+ },
189
+ required: []
190
+ }
191
+ },
192
+ {
193
+ name: "tickets_update",
194
+ type: "update",
195
+ description: `Update a ticket in the "${projectSlug}" project by appending content with timestamp`,
196
+ inputSchema: {
197
+ type: "object",
198
+ properties: {
199
+ ticketSlug: {
200
+ type: "string",
201
+ description: "Ticket number (e.g., '102') or full slug (e.g., '102-fix-auth')"
202
+ },
203
+ content: {
204
+ type: "string",
205
+ description: "Update content to append to the ticket (markdown supported)"
206
+ },
207
+ agent: {
208
+ type: "string",
209
+ description: "Optional: Reassign ticket to this agent. Use empty string '' to unassign."
210
+ }
211
+ },
212
+ required: ["ticketSlug", "content"]
213
+ }
214
+ }
215
+ ];
216
+ }
217
+ function getMemoryToolDefinitions() {
218
+ return [
219
+ {
220
+ name: "memories_load",
221
+ type: "load",
222
+ description: `Load project memories: returns all rules (always) + relevant history (by query or recent). Use [MEMORY_LOAD] in prompts for automatic loading.`,
223
+ inputSchema: {
224
+ type: "object",
225
+ properties: {
226
+ query: {
227
+ type: "string",
228
+ description: "Optional: search query to filter agent memories. If not provided, returns recent memories."
229
+ }
230
+ },
231
+ required: []
232
+ }
233
+ },
234
+ {
235
+ name: "memories_load_extreme",
236
+ type: "load_extreme",
237
+ description: `Load project memories with FULL ticket context. Returns all matching memories, and for each memory linked to a ticket, includes the complete ticket content (tasks, updates, decisions). Use when you need deep historical context about past work.`,
238
+ inputSchema: {
239
+ type: "object",
240
+ properties: {
241
+ query: {
242
+ type: "string",
243
+ description: "Optional: search query to filter agent memories. If not provided, returns recent memories."
244
+ }
245
+ },
246
+ required: []
247
+ }
248
+ },
249
+ {
250
+ name: "memories_add",
251
+ type: "add",
252
+ description: `Record a project memory. Optionally links to working ticket if exactly one exists. Use for key decisions, patterns discovered, or important context.`,
253
+ inputSchema: {
254
+ type: "object",
255
+ properties: {
256
+ content: {
257
+ type: "string",
258
+ description: "Memory content to record. Be concise and focused on key decisions, patterns, or context."
259
+ }
260
+ },
261
+ required: ["content"]
262
+ }
263
+ },
264
+ {
265
+ name: "memories_list_all",
266
+ type: "list_all",
267
+ description: `List ALL project memories (rules and agent history). Returns complete memory list with IDs for review/cleanup. Use for memory audits or before bulk operations.`,
268
+ inputSchema: {
269
+ type: "object",
270
+ properties: {
271
+ typeFilter: {
272
+ type: "string",
273
+ enum: ["all", "user", "agent"],
274
+ description: "Optional: Filter by memory type. 'user' = rules, 'agent' = history. Default: 'all'"
275
+ }
276
+ },
277
+ required: []
278
+ }
279
+ }
280
+ ];
281
+ }
282
+ function getPromptToolDefinitions() {
283
+ return [
284
+ {
285
+ name: "prompts_run",
286
+ description: "Execute a prompt by slug. Use prompts_list to list available prompts.",
287
+ inputSchema: {
288
+ type: "object",
289
+ properties: {
290
+ slug: {
291
+ type: "string",
292
+ description: "Prompt slug to execute (e.g., 'code-review', 'plan')"
293
+ }
294
+ },
295
+ required: ["slug"]
296
+ }
297
+ },
298
+ {
299
+ name: "prompts_list",
300
+ description: "List all available prompts",
301
+ inputSchema: {
302
+ type: "object",
303
+ properties: {
304
+ search: {
305
+ type: "string",
306
+ description: "Optional search term to filter prompts by name or description (case-insensitive)"
307
+ }
308
+ }
309
+ }
310
+ },
311
+ {
312
+ name: "prompts_update",
313
+ description: "Read or update a prompt, snippet, or template. Without content: returns resource with context. With content: saves new version.",
314
+ inputSchema: {
315
+ type: "object",
316
+ properties: {
317
+ slug: {
318
+ type: "string",
319
+ description: "Resource slug to read or update. Searches prompts first, then snippets, then templates. Supports fuzzy matching."
320
+ },
321
+ content: {
322
+ type: "string",
323
+ description: "New resource content (omit for read-only mode)"
324
+ }
325
+ },
326
+ required: ["slug"]
327
+ }
328
+ }
329
+ ];
330
+ }
331
+
25
332
  // src/types.ts
26
333
  function isAccessDenied(result) {
27
334
  return typeof result === "object" && result !== null && "accessDenied" in result && result.accessDenied === true;
@@ -205,1636 +512,1176 @@ async function fetchAndExecuteAccountScopedPrompt(promptSlug, config, convexClie
205
512
  };
206
513
  }
207
514
 
208
- // src/server.ts
209
- function buildAuthArgs(config) {
210
- return { projectToken: config.projectToken };
211
- }
212
- function resolveAgent(paramAgent) {
213
- if (paramAgent) return paramAgent;
214
- const envAgent = process.env.PPM_AGENT_NAME;
215
- if (envAgent) return envAgent;
216
- return void 0;
217
- }
218
- var SYSTEM_TOOLS = [
219
- // All tools are now project-scoped (see dynamic*Tools arrays below)
220
- ];
221
- async function startServer(config, convexClientRaw) {
222
- const convexClient = convexClientRaw;
223
- console.error("[MCP] Validating project token...");
224
- const validation = await validateProjectToken(convexClient, config.projectToken);
225
- if (!validation.valid) {
226
- throw new Error(`Invalid project token: ${validation.error}`);
227
- }
228
- const tokenProjectSlug = validation.projectSlug;
229
- console.error(`[MCP] Project token validated for project "${tokenProjectSlug}"`);
230
- console.error("[MCP] Fetching prompt metadata...");
231
- const accountScopedPrompts = await fetchAccountScopedPromptMetadataByToken(
232
- convexClient,
233
- config.projectToken
234
- );
235
- console.error(`[MCP] Project token mode: loaded ${accountScopedPrompts.length} prompts`);
236
- console.error(`[MCP] Found ${accountScopedPrompts.length} account-scoped prompts (global tools)`);
237
- console.error(`[MCP] Ticket project scope: ${tokenProjectSlug} (token-scoped)`);
238
- if (accountScopedPrompts.length === 0) {
239
- console.error(
240
- "[MCP] WARNING: No prompts found. Create prompts in the 'prompts' section to expose them via MCP."
241
- );
242
- }
243
- const projectSlug = tokenProjectSlug;
244
- const dynamicTicketTools = [
245
- // Unified work command (replaces both tickets_open and tickets_work)
246
- {
247
- name: `tickets_work`,
248
- description: `Get work from the "${projectSlug}" project. With no args: gets next ticket from open queue. With ticket slug/number: opens or resumes that specific ticket.`,
249
- slashDescription: `Get next ticket from queue, or work on specific ticket by slug/number`,
250
- projectSlug,
251
- type: "work"
252
- },
253
- {
254
- name: `tickets_close`,
255
- description: `Mark a working ticket as completed in the "${projectSlug}" project. If a hook is configured, first call returns instructions - then call again with metadata.`,
256
- slashDescription: `Mark a working ticket as completed`,
257
- projectSlug,
258
- type: "close"
259
- },
260
- {
261
- name: `tickets_pause`,
262
- description: `Pause a working ticket and return to queue. Before calling, review the full session context and generate comprehensive checkpoint content that enables seamless handoff to another agent. Include: completed tasks with specifics, key decisions and rationale, sticking points and resolutions, user preferences discovered, remaining work with file:line references, and open questions.`,
263
- slashDescription: `Pause working ticket with checkpoint content (working \u2192 open)`,
264
- projectSlug,
265
- type: "pause"
266
- },
267
- {
268
- name: `tickets_create`,
269
- description: `Create a new ticket in the "${projectSlug}" project queue`,
270
- slashDescription: `Create a new ticket in the backlog queue`,
271
- projectSlug,
272
- type: "create"
273
- },
274
- {
275
- name: `tickets_search`,
276
- description: `Search for tickets by content in the "${projectSlug}" project`,
277
- slashDescription: `Search for tickets by content`,
278
- projectSlug,
279
- type: "search"
280
- },
281
- {
282
- name: `tickets_get`,
283
- description: `Get a specific ticket by number or slug from "${projectSlug}" (read-only)`,
284
- slashDescription: `Get a specific ticket by number or slug (read-only)`,
285
- projectSlug,
286
- type: "get"
287
- },
288
- {
289
- name: `tickets_list`,
290
- description: `List active tickets in the "${projectSlug}" project (backlog + open + working)`,
291
- slashDescription: `List active tickets (backlog + open + working)`,
292
- projectSlug,
293
- type: "list"
294
- },
295
- {
296
- name: `tickets_update`,
297
- description: `Update a ticket in the "${projectSlug}" project by appending content with timestamp`,
298
- slashDescription: `Update a ticket by appending content with timestamp`,
299
- projectSlug,
300
- type: "update"
301
- }
302
- ];
303
- console.error(`[MCP] Registering ${dynamicTicketTools.length} ticket tools...`);
304
- const dynamicMemoryTools = [
305
- {
306
- name: `memories_load`,
307
- description: `Load project memories: returns all rules (always) + relevant history (by query or recent). Use [MEMORY_LOAD] in prompts for automatic loading.`,
308
- slashDescription: `Load project memories (rules + history)`,
309
- projectSlug,
310
- type: "load"
311
- },
312
- {
313
- name: `memories_load_extreme`,
314
- description: `Load project memories with FULL ticket context. Returns all matching memories, and for each memory linked to a ticket, includes the complete ticket content (tasks, updates, decisions). Use when you need deep historical context about past work.`,
315
- slashDescription: `Load memories with full ticket context`,
316
- projectSlug,
317
- type: "load_extreme"
318
- },
319
- {
320
- name: `memories_add`,
321
- description: `Record a project memory. Optionally links to working ticket if exactly one exists. Use for key decisions, patterns discovered, or important context.`,
322
- slashDescription: `Add memory to project (auto-links to working ticket if one exists)`,
323
- projectSlug,
324
- type: "add"
325
- },
326
- {
327
- name: `memories_list_all`,
328
- description: `List ALL project memories (rules and agent history). Returns complete memory list with IDs for review/cleanup. Use for memory audits or before bulk operations.`,
329
- slashDescription: `List all memories with IDs for review`,
330
- projectSlug,
331
- type: "list_all"
332
- }
333
- ];
334
- console.error(`[MCP] Registering ${dynamicMemoryTools.length} memory tools...`);
335
- const globalPromptTools = [
336
- {
337
- name: "prompts_run",
338
- description: "Execute a prompt by slug. Use prompts_list to list available prompts.",
339
- slashDescription: "Execute a prompt by slug. Use prompts_list to list available prompts."
340
- },
341
- {
342
- name: "prompts_list",
343
- description: "List all available prompts",
344
- slashDescription: "List all available prompts"
345
- },
346
- {
347
- name: "prompts_update",
348
- description: "Read or update a prompt, snippet, or template. Without content: returns resource with context. With content: saves new version.",
349
- slashDescription: "Read or update a prompt, snippet, or template by slug"
350
- }
351
- ];
352
- console.error(`[MCP] Registering ${globalPromptTools.length} global prompt tools...`);
353
- const dynamicPromptTools = [];
354
- for (const prompt of accountScopedPrompts) {
355
- const folderPrefix = prompt.folderPath ? `[${prompt.folderPath}] ` : "";
356
- const baseDescription = prompt.description || `Execute the "${prompt.slug}" prompt`;
357
- dynamicPromptTools.push({
358
- name: prompt.slug,
359
- // Global tool name, no project prefix
360
- description: folderPrefix + baseDescription,
361
- promptSlug: prompt.slug
362
- });
515
+ // src/handlers/prompts.ts
516
+ async function handlePromptsRun(args, config, convexClient) {
517
+ const parsedArgs = parsePromptsRunArgs(args);
518
+ if (!parsedArgs) {
519
+ return {
520
+ content: [
521
+ {
522
+ type: "text",
523
+ text: `Error: Missing 'slug' parameter. Provide prompt slug (e.g., 'code-review', 'plan').
524
+
525
+ Use \`prompts_list\` to list available prompts.`
526
+ }
527
+ ],
528
+ isError: true
529
+ };
363
530
  }
364
- console.error(`[MCP] Registering ${dynamicPromptTools.length} per-prompt tools (global)...`);
365
- const promptNames = /* @__PURE__ */ new Set();
366
- const duplicates = [];
367
- accountScopedPrompts.forEach((p) => {
368
- if (promptNames.has(p.slug)) {
369
- duplicates.push(p.slug);
370
- }
371
- promptNames.add(p.slug);
372
- });
373
- if (duplicates.length > 0) {
374
- console.error(
375
- `[MCP] WARNING: Duplicate prompt slugs detected: ${duplicates.join(", ")}. Only the first occurrence will be registered.`
531
+ const { slug: promptSlug } = parsedArgs;
532
+ try {
533
+ const result = await fetchAndExecuteAccountScopedPrompt(
534
+ promptSlug,
535
+ config,
536
+ convexClient
376
537
  );
377
- }
378
- const server = new Server(
379
- {
380
- name: "ppm-mcp-server",
381
- version: "3.1.0"
382
- },
383
- {
384
- capabilities: {
385
- prompts: {},
386
- tools: {}
387
- }
388
- }
389
- );
390
- server.setRequestHandler(ListPromptsRequestSchema, async () => {
391
- const ticketPromptSchemas = dynamicTicketTools.map((tt) => ({
392
- name: tt.name,
393
- description: tt.slashDescription
394
- }));
395
- const memoryPromptSchemas = dynamicMemoryTools.map((mt) => ({
396
- name: mt.name,
397
- description: mt.slashDescription
398
- }));
399
- const promptToolSchemas = globalPromptTools.map((st) => ({
400
- name: st.name,
401
- description: st.slashDescription
402
- }));
538
+ const promptText = result.messages.map((msg) => msg.content.text).join("\n\n");
403
539
  return {
404
- prompts: [
405
- ...promptToolSchemas,
406
- // prompts_run, prompts_list, prompts_update (global)
407
- ...ticketPromptSchemas,
408
- // tickets_* (token-scoped)
409
- ...memoryPromptSchemas
410
- // memories_* (token-scoped)
540
+ content: [
541
+ {
542
+ type: "text",
543
+ text: promptText
544
+ }
411
545
  ]
412
546
  };
413
- });
414
- server.setRequestHandler(GetPromptRequestSchema, async (request) => {
415
- const promptName = request.params.name;
416
- const systemTool = SYSTEM_TOOLS.find((st) => st.name === promptName);
417
- if (systemTool) {
547
+ } catch (error) {
548
+ if (error instanceof AmbiguousPromptError) {
549
+ const suggestionsList = error.suggestions.map((s) => ` \u2022 ${s}`).join("\n");
550
+ console.error(`[MCP] prompts_run ambiguous match:`, error.suggestions);
418
551
  return {
419
- description: systemTool.description,
420
- messages: [
552
+ content: [
421
553
  {
422
- role: "user",
423
- content: {
424
- type: "text",
425
- text: systemTool.promptContent
426
- }
554
+ type: "text",
555
+ text: `Multiple prompts match "${promptSlug}". Please specify one of:
556
+
557
+ ${suggestionsList}
558
+
559
+ Example: \`prompts_run ${error.suggestions[0]}\``
427
560
  }
428
- ]
561
+ ],
562
+ isError: true
429
563
  };
430
564
  }
431
- const ticketTool = dynamicTicketTools.find((tt) => tt.name === promptName);
432
- const ticketWorkMatch = promptName.match(/^tickets_work\s+(.+)$/);
433
- if (ticketTool || ticketWorkMatch) {
434
- let promptContent;
435
- let description;
436
- if (ticketWorkMatch) {
437
- const ticketArg = ticketWorkMatch[1].trim();
438
- description = `Work on ticket "${ticketArg}" from "${projectSlug}"`;
439
- promptContent = `Get work on ticket "${ticketArg}" from the "${projectSlug}" project.
565
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
566
+ console.error(`[MCP] prompts_run error:`, error);
567
+ return {
568
+ content: [
569
+ {
570
+ type: "text",
571
+ text: `Error executing prompt "${promptSlug}": ${errorMessage}`
572
+ }
573
+ ],
574
+ isError: true
575
+ };
576
+ }
577
+ }
578
+ async function handlePromptsList(args, _config, _convexClient, accountScopedPrompts) {
579
+ const { search: searchTerm } = parsePromptsListArgs(args);
580
+ let filteredPrompts = [...accountScopedPrompts];
581
+ if (searchTerm) {
582
+ const lowerSearch = searchTerm.toLowerCase();
583
+ filteredPrompts = filteredPrompts.filter(
584
+ (p) => p.slug.toLowerCase().includes(lowerSearch) || p.description?.toLowerCase().includes(lowerSearch)
585
+ );
586
+ }
587
+ filteredPrompts.sort((a, b) => a.slug.localeCompare(b.slug));
588
+ const userPromptList = filteredPrompts.map((p) => {
589
+ const desc = p.description || "No description";
590
+ return `\u2022 ${p.slug}
591
+ Description: ${desc}`;
592
+ }).join("\n\n");
593
+ const summary = searchTerm ? `Found ${filteredPrompts.length} prompt(s) matching "${searchTerm}":` : `Available prompts (${filteredPrompts.length} total):`;
594
+ return {
595
+ content: [
596
+ {
597
+ type: "text",
598
+ text: userPromptList ? `${summary}
440
599
 
441
- Call the \`tickets_work\` tool with ticketSlug: "${ticketArg}".
600
+ ${userPromptList}` : `${summary}
442
601
 
443
- This will open the ticket if it's in the open queue, or resume it if already working.`;
444
- } else if (ticketTool.type === "work") {
445
- description = ticketTool.description;
446
- promptContent = `Get work from the "${ticketTool.projectSlug}" project.
602
+ No prompts found.`
603
+ }
604
+ ]
605
+ };
606
+ }
607
+ async function handlePromptsUpdate(args, config, convexClient) {
608
+ const parsedArgs = parsePromptsUpdateArgs(args);
609
+ if (!parsedArgs) {
610
+ return {
611
+ content: [
612
+ {
613
+ type: "text",
614
+ text: `Error: Missing 'slug' parameter. Provide prompt slug to read or update.
447
615
 
448
- Call the \`tickets_work\` tool to get the next ticket from the open queue.
616
+ Usage:
617
+ - Read mode: prompts_update { slug: "my-prompt" }
618
+ - Write mode: prompts_update { slug: "my-prompt", content: "new content..." }`
619
+ }
620
+ ],
621
+ isError: true
622
+ };
623
+ }
624
+ try {
625
+ const typedClient = convexClient;
626
+ const result = await typedClient.mutation(
627
+ "mcp_prompts:updateMcpPrompt",
628
+ {
629
+ ...buildAuthArgs(config),
630
+ promptSlug: parsedArgs.slug,
631
+ content: parsedArgs.content
632
+ }
633
+ );
634
+ if (!result.success) {
635
+ const errorResult = result;
636
+ if (errorResult.suggestions) {
637
+ const suggestionsList = errorResult.suggestions.map((s) => ` \u2022 ${s}`).join("\n");
638
+ return {
639
+ content: [
640
+ {
641
+ type: "text",
642
+ text: `${errorResult.error}
449
643
 
450
- You can also specify a ticket: /ppm:tickets_work <number-or-slug>
644
+ Did you mean one of these?
645
+ ${suggestionsList}`
646
+ }
647
+ ],
648
+ isError: true
649
+ };
650
+ }
651
+ return {
652
+ content: [{ type: "text", text: errorResult.error }],
653
+ isError: true
654
+ };
655
+ }
656
+ if (result.mode === "read") {
657
+ const readResult = result;
658
+ const matchNote = readResult.matchType !== "exact" ? `
659
+ _Note: Matched via ${readResult.matchType} match_` : "";
660
+ const aiTagNotice = readResult.hasAiTags ? `
451
661
 
452
- This unified command handles both opening new tickets and resuming in-progress work.`;
453
- } else if (ticketTool.type === "create") {
454
- description = ticketTool.description;
455
- promptContent = `Create a new ticket in the "${ticketTool.projectSlug}" project queue.
662
+ ## \u26A1 AI Tags Detected
663
+ This ${readResult.resourceType} contains AI tags that need processing: **${readResult.aiTags?.join(", ")}**
456
664
 
457
- ## How This Works
458
- The user provides **instructions** (not raw content). You interpret those instructions to generate the ticket.
665
+ Process these tags by reading the instructions in each tag, generating appropriate content, and saving the result with the content parameter.` : "";
666
+ const versionInfo = readResult.version !== void 0 ? ` (v${readResult.version})` : "";
667
+ const resourceLabel = readResult.resourceType.charAt(0).toUpperCase() + readResult.resourceType.slice(1);
668
+ const contextSection = readResult.context ? `
459
669
 
460
- ## Instructions
461
- 1. **Read the user's request** - they may reference a file, ask you to summarize a session, or describe what they want
462
- 2. **Process the input** - read files, extract relevant sections, summarize as needed
463
- 3. **Generate ticket content** with a clear, descriptive title as the first line
464
- 4. **Call the tool** with the generated content
670
+ ## Available Resources
671
+ \`\`\`json
672
+ ${JSON.stringify(readResult.context, null, 2)}
673
+ \`\`\`` : "";
674
+ return {
675
+ content: [
676
+ {
677
+ type: "text",
678
+ text: `# ${resourceLabel}: ${readResult.slug}${versionInfo}${matchNote}${aiTagNotice}
465
679
 
466
- ## Content Format
467
- \`\`\`
468
- [Clear descriptive title - this becomes the slug]
680
+ ## Description
681
+ ${readResult.description || "No description"}
469
682
 
470
- [Body content - tasks, description, context, etc.]
683
+ ## Content
471
684
  \`\`\`
685
+ ${readResult.content}
686
+ \`\`\`${contextSection}
472
687
 
473
- ## Examples
474
- - "create a ticket from /path/to/plan.md" \u2192 Read file, use as ticket content
475
- - "summarize our brainstorm into a ticket" \u2192 Extract key points from conversation
476
- - "create a ticket for the auth bug we discussed" \u2192 Generate from session context
477
- - "just the tasks from this file" \u2192 Extract only task items
478
-
479
- Call the \`${ticketTool.name}\` tool with the final generated content.`;
480
- } else if (ticketTool.type === "close") {
481
- description = ticketTool.description;
482
- promptContent = `Close a working ticket in the "${ticketTool.projectSlug}" project.
688
+ ${readResult.editingGuidance}
483
689
 
484
- Call the \`${ticketTool.name}\` tool with the ticket number or slug.`;
485
- } else {
486
- description = ticketTool.description;
487
- promptContent = `Use the \`${ticketTool.name}\` tool.`;
488
- }
489
- return {
490
- description,
491
- messages: [
492
- {
493
- role: "user",
494
- content: {
495
- type: "text",
496
- text: promptContent
497
- }
690
+ ---
691
+ _To update this ${readResult.resourceType}, call prompts_update with content parameter._`
498
692
  }
499
693
  ]
500
694
  };
501
695
  }
502
- if (promptName === "prompts_list") {
696
+ if (result.mode === "write") {
697
+ const writeResult = result;
698
+ const resourceLabel = writeResult.resourceType.charAt(0).toUpperCase() + writeResult.resourceType.slice(1);
699
+ const versionInfo = writeResult.version !== void 0 ? `
700
+ Version: ${writeResult.version}` : "";
701
+ const updatedInfo = writeResult.updatedAt ? `
702
+ Updated: ${writeResult.updatedAt}` : "";
503
703
  return {
504
- description: "List all available prompts",
505
- messages: [
704
+ content: [
506
705
  {
507
- role: "user",
508
- content: {
509
- type: "text",
510
- text: `List all available prompts.
706
+ type: "text",
707
+ text: `\u2705 ${writeResult.message}
511
708
 
512
- Call the \`prompts_list\` tool with optional \`search\` parameter to filter results.`
513
- }
709
+ ${resourceLabel}: ${writeResult.slug}${versionInfo}${updatedInfo}`
514
710
  }
515
711
  ]
516
712
  };
517
713
  }
518
- const runPromptMatch = promptName.match(/^prompts_run\s+(.+)$/);
519
- if (promptName === "prompts_run" || runPromptMatch) {
520
- let promptContent;
521
- let description;
522
- if (runPromptMatch) {
523
- const promptSlug = runPromptMatch[1].trim();
524
- description = `Execute prompt "${promptSlug}"`;
525
- promptContent = `Execute the "${promptSlug}" prompt.
714
+ return {
715
+ content: [{ type: "text", text: "Unknown response format" }],
716
+ isError: true
717
+ };
718
+ } catch (error) {
719
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
720
+ console.error(`[MCP] prompts_update error:`, error);
721
+ return {
722
+ content: [
723
+ {
724
+ type: "text",
725
+ text: `Error updating prompt: ${errorMessage}`
726
+ }
727
+ ],
728
+ isError: true
729
+ };
730
+ }
731
+ }
526
732
 
527
- Call the \`prompts_run\` tool with slug: "${promptSlug}".`;
528
- } else {
529
- description = "Execute a prompt by slug";
530
- promptContent = `Execute a prompt by slug.
733
+ // src/handlers/tickets.ts
734
+ async function handleTicketsWork(args, config, convexClient, projectSlug) {
735
+ const { ticketSlug, agent } = parseWorkArgs(args);
736
+ const resolvedAgent = resolveAgent(agent);
737
+ const typedClient = convexClient;
738
+ try {
739
+ const result = await typedClient.mutation(
740
+ "mcp_tickets:workMcpTicket",
741
+ {
742
+ ...buildAuthArgs(config),
743
+ projectSlug,
744
+ ticketSlug,
745
+ agentId: resolvedAgent
746
+ }
747
+ );
748
+ if (!result) {
749
+ const agentContext = resolvedAgent ? ` for agent "${resolvedAgent}"` : "";
750
+ const message = ticketSlug ? `Ticket "${ticketSlug}" not found or not in open/working status in project "${projectSlug}"${agentContext}.` : `No open tickets${agentContext} in project "${projectSlug}".`;
751
+ return {
752
+ content: [{ type: "text", text: message }]
753
+ };
754
+ }
755
+ if (isAccessDenied(result)) {
756
+ return {
757
+ content: [{ type: "text", text: `Access denied: ${result.error}` }],
758
+ isError: true
759
+ };
760
+ }
761
+ const workResult = result;
762
+ const startedInfo = workResult.startedAt ? `
763
+ Started: ${new Date(workResult.startedAt).toISOString()}` : "";
764
+ const pausedNotice = workResult.pausedAt ? `
531
765
 
532
- ## Usage
533
- Call the \`prompts_run\` tool with the prompt slug.
766
+ **\u26A0\uFE0F PAUSED TICKET HANDOFF**: This ticket was paused on ${new Date(workResult.pausedAt).toISOString()}. A previous agent checkpointed their progress. Review the checkpoint content below (look for "## Progress", "## Context", "## Remaining", "## Blockers" sections) and resume where they left off.
767
+ ` : "";
768
+ const statusNote = workResult.wasOpened ? `_Ticket moved to working status. ${workResult.remainingTickets} ticket(s) remaining in queue._` : `_Resuming work on this ticket. ${workResult.remainingTickets} ticket(s) in queue._`;
769
+ return {
770
+ content: [
771
+ {
772
+ type: "text",
773
+ text: `# Ticket: ${workResult.slug} [WORKING]${startedInfo}${pausedNotice}
534
774
 
535
- ## Available Prompts
536
- Use \`prompts_list\` to list all available prompts.
775
+ ${workResult.content}
537
776
 
538
- ## Example
539
- /ppm:prompts_run code-review
777
+ ---
778
+ ${statusNote}`
779
+ }
780
+ ]
781
+ };
782
+ } catch (error) {
783
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
784
+ console.error(`[MCP] tickets_work error:`, error);
785
+ return {
786
+ content: [{ type: "text", text: `Error getting work: ${errorMessage}` }],
787
+ isError: true
788
+ };
789
+ }
790
+ }
791
+ async function handleTicketsCreate(args, config, convexClient, projectSlug) {
792
+ const parsedArgs = parseCreateArgs(args);
793
+ if (!parsedArgs) {
794
+ return {
795
+ content: [
796
+ {
797
+ type: "text",
798
+ text: `Error: Missing content parameter. Provide the ticket content (first line becomes the slug).`
799
+ }
800
+ ],
801
+ isError: true
802
+ };
803
+ }
804
+ const { content, agent } = parsedArgs;
805
+ const resolvedAgent = resolveAgent(agent);
806
+ const typedClient = convexClient;
807
+ try {
808
+ const result = await typedClient.mutation(
809
+ "mcp_tickets:createMcpTicket",
810
+ {
811
+ ...buildAuthArgs(config),
812
+ projectSlug,
813
+ content,
814
+ agentId: resolvedAgent
815
+ }
816
+ );
817
+ const agentInfo = result.agentId ? `
818
+ Assigned to: ${result.agentId}` : "";
819
+ return {
820
+ content: [
821
+ {
822
+ type: "text",
823
+ text: `\u2705 Created ticket [${result.slug}] in backlog for project "${projectSlug}".${agentInfo}
824
+
825
+ Position: #${result.position} of ${result.totalBacklog} backlog tickets
826
+ Preview: ${result.preview}
827
+
828
+ _Ticket created in backlog. Use \`tickets_work ${result.slug}\` to move it to working status._`
829
+ }
830
+ ]
831
+ };
832
+ } catch (error) {
833
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
834
+ console.error(`[MCP] tickets_create error:`, error);
835
+ return {
836
+ content: [{ type: "text", text: `Error creating ticket: ${errorMessage}` }],
837
+ isError: true
838
+ };
839
+ }
840
+ }
841
+ async function handleTicketsClose(args, config, convexClient, projectSlug) {
842
+ const parsedArgs = parseCloseArgs(args);
843
+ if (!parsedArgs) {
844
+ return {
845
+ content: [
846
+ {
847
+ type: "text",
848
+ text: `Error: Missing ticketSlug parameter. Usage: Provide a ticket number (e.g., "102") or full slug (e.g., "102-fix-auth").`
849
+ }
850
+ ],
851
+ isError: true
852
+ };
853
+ }
854
+ const { ticketSlug, metadata } = parsedArgs;
855
+ const typedClient = convexClient;
856
+ try {
857
+ const result = await typedClient.mutation(
858
+ "mcp_tickets:closeMcpTicket",
859
+ {
860
+ ...buildAuthArgs(config),
861
+ projectSlug,
862
+ ticketSlug,
863
+ metadata
864
+ }
865
+ );
866
+ let responseText = `\u2705 Ticket [${result.slug}] closed in project "${projectSlug}".
867
+
868
+ Closed at: ${new Date(result.closedAt).toISOString()}`;
869
+ if (result.metadata && Object.keys(result.metadata).length > 0) {
870
+ responseText += `
540
871
 
541
- This will execute the "code-review" prompt.`;
872
+ **Metadata saved:**`;
873
+ for (const [key, value] of Object.entries(result.metadata)) {
874
+ responseText += `
875
+ - ${key}: ${JSON.stringify(value)}`;
542
876
  }
877
+ }
878
+ responseText += `
879
+
880
+ _Reminder: Ensure all embedded \`[RUN_PROMPT ...]\` directives were executed before closing._`;
881
+ return {
882
+ content: [{ type: "text", text: responseText }]
883
+ };
884
+ } catch (error) {
885
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
886
+ console.error(`[MCP] tickets_close error:`, error);
887
+ return {
888
+ content: [{ type: "text", text: `Error closing ticket: ${errorMessage}` }],
889
+ isError: true
890
+ };
891
+ }
892
+ }
893
+ async function handleTicketsPause(args, config, convexClient, projectSlug) {
894
+ const parsedArgs = parsePauseArgs(args);
895
+ if (!parsedArgs) {
896
+ const rawArgs = args;
897
+ const hasTicketSlug = typeof rawArgs?.ticketSlug === "string" && rawArgs.ticketSlug;
898
+ const hasContent = typeof rawArgs?.content === "string" && rawArgs.content;
899
+ if (!hasTicketSlug) {
543
900
  return {
544
- description,
545
- messages: [
901
+ content: [
546
902
  {
547
- role: "user",
548
- content: {
549
- type: "text",
550
- text: promptContent
551
- }
903
+ type: "text",
904
+ text: `Error: Missing ticketSlug parameter. Provide a ticket number (e.g., "102") or full slug.`
552
905
  }
553
- ]
906
+ ],
907
+ isError: true
554
908
  };
555
909
  }
556
- throw new Error(`Unknown prompt: ${promptName}. Use prompts_run to execute prompts.`);
557
- });
558
- server.setRequestHandler(ListToolsRequestSchema, async () => {
559
- const tools = [
560
- // System tools with full input schemas
561
- ...SYSTEM_TOOLS.map((st) => ({
562
- name: st.name,
563
- description: st.description,
564
- inputSchema: st.inputSchema
565
- })),
566
- // Dynamic ticket tools per project (Ticket 135, 149, 151, 153, unified work)
567
- ...dynamicTicketTools.map((tt) => {
568
- let inputSchema;
569
- if (tt.type === "close") {
570
- inputSchema = {
571
- type: "object",
572
- properties: {
573
- ticketSlug: {
574
- type: "string",
575
- description: "Ticket number (e.g., '102') or full slug (e.g., '102-fix-auth')"
576
- },
577
- metadata: {
578
- type: "object",
579
- description: "Optional: Key-value metadata to store with the closed ticket (e.g., evaluation scores, completion notes).",
580
- additionalProperties: {
581
- oneOf: [
582
- { type: "string" },
583
- { type: "number" },
584
- { type: "boolean" }
585
- ]
586
- }
587
- }
588
- },
589
- required: ["ticketSlug"]
590
- };
591
- } else if (tt.type === "pause") {
592
- inputSchema = {
593
- type: "object",
594
- properties: {
595
- ticketSlug: {
596
- type: "string",
597
- description: "Ticket number (e.g., '102') or full slug (e.g., '102-fix-auth')"
598
- },
599
- content: {
600
- type: "string",
601
- description: "Comprehensive checkpoint for handoff. Review the session and include: ## Progress (completed tasks with specifics, decisions made and why) ## Context (user preferences, edge cases discovered, key feedback received) ## Remaining (specific next steps, file:line references if applicable) ## Blockers (unresolved issues, open questions). This enables the next agent to continue seamlessly."
602
- }
603
- },
604
- required: ["ticketSlug", "content"]
605
- };
606
- } else if (tt.type === "work") {
607
- inputSchema = {
608
- type: "object",
609
- properties: {
610
- ticketSlug: {
611
- type: "string",
612
- description: "Optional: Ticket number (e.g., '102') or full slug. If not provided, gets next ticket from open queue."
613
- },
614
- agent: {
615
- type: "string",
616
- description: "Optional: Agent slug to filter queue. Overrides PPM_AGENT_NAME env var. Omit for owner mode (no filtering)."
617
- }
618
- },
619
- required: []
620
- };
621
- } else if (tt.type === "create") {
622
- inputSchema = {
623
- type: "object",
624
- properties: {
625
- content: {
626
- type: "string",
627
- description: "The generated ticket content. First line should be a clear descriptive title (becomes the slug). Body contains tasks, description, context as needed."
628
- },
629
- agent: {
630
- type: "string",
631
- description: "Optional: Agent slug to assign ticket to. Overrides PPM_AGENT_NAME env var. Omit for unassigned."
632
- }
633
- },
634
- required: ["content"]
635
- };
636
- } else if (tt.type === "search") {
637
- inputSchema = {
638
- type: "object",
639
- properties: {
640
- query: {
641
- type: "string",
642
- description: "Search query (min 3 characters)"
643
- },
644
- agent: {
645
- type: "string",
646
- description: "Optional: Agent slug to filter results. Overrides PPM_AGENT_NAME env var. Omit for all tickets."
647
- }
648
- },
649
- required: ["query"]
650
- };
651
- } else if (tt.type === "get") {
652
- inputSchema = {
653
- type: "object",
654
- properties: {
655
- ticketSlug: {
656
- type: "string",
657
- description: "Ticket number (e.g., '102') or full slug"
658
- }
659
- },
660
- required: ["ticketSlug"]
661
- };
662
- } else if (tt.type === "list") {
663
- inputSchema = {
664
- type: "object",
665
- properties: {
666
- agent: {
667
- type: "string",
668
- description: "Optional: Agent slug to filter tickets. Overrides PPM_AGENT_NAME env var. Omit for all tickets."
669
- }
670
- },
671
- required: []
672
- };
673
- } else if (tt.type === "update") {
674
- inputSchema = {
675
- type: "object",
676
- properties: {
677
- ticketSlug: {
678
- type: "string",
679
- description: "Ticket number (e.g., '102') or full slug (e.g., '102-fix-auth')"
680
- },
681
- content: {
682
- type: "string",
683
- description: "Update content to append to the ticket (markdown supported)"
684
- },
685
- agent: {
686
- type: "string",
687
- description: "Optional: Reassign ticket to this agent. Use empty string '' to unassign."
688
- }
689
- },
690
- required: ["ticketSlug", "content"]
691
- };
692
- } else {
693
- inputSchema = {
694
- type: "object",
695
- properties: {},
696
- required: []
697
- };
698
- }
699
- return {
700
- name: tt.name,
701
- description: tt.description,
702
- inputSchema
703
- };
704
- }),
705
- // Global prompts_run tool
706
- {
707
- name: "prompts_run",
708
- description: "Execute a prompt by slug. Use prompts_list to list available prompts.",
709
- inputSchema: {
710
- type: "object",
711
- properties: {
712
- slug: {
713
- type: "string",
714
- description: "Prompt slug to execute (e.g., 'code-review', 'plan')"
715
- }
716
- },
717
- required: ["slug"]
718
- }
719
- },
720
- // Global prompts_list tool
721
- {
722
- name: "prompts_list",
723
- description: "List all available prompts",
724
- inputSchema: {
725
- type: "object",
726
- properties: {
727
- search: {
728
- type: "string",
729
- description: "Optional search term to filter prompts by name or description (case-insensitive)"
730
- }
910
+ if (!hasContent) {
911
+ return {
912
+ content: [
913
+ {
914
+ type: "text",
915
+ text: `Error: Missing content parameter. Describe what's done, what remains, and any blockers.`
731
916
  }
732
- }
733
- },
734
- // Global prompts_update tool (CLI-based prompt/snippet/template editing)
917
+ ],
918
+ isError: true
919
+ };
920
+ }
921
+ return {
922
+ content: [{ type: "text", text: `Error: Missing required parameters.` }],
923
+ isError: true
924
+ };
925
+ }
926
+ const { ticketSlug, content } = parsedArgs;
927
+ const typedClient = convexClient;
928
+ try {
929
+ const result = await typedClient.mutation(
930
+ "mcp_tickets:pauseMcpTicket",
735
931
  {
736
- name: "prompts_update",
737
- description: "Read or update a prompt, snippet, or template. Without content: returns resource with context. With content: saves new version.",
738
- inputSchema: {
739
- type: "object",
740
- properties: {
741
- slug: {
742
- type: "string",
743
- description: "Resource slug to read or update. Searches prompts first, then snippets, then templates. Supports fuzzy matching."
744
- },
745
- content: {
746
- type: "string",
747
- description: "New resource content (omit for read-only mode)"
748
- }
749
- },
750
- required: ["slug"]
751
- }
752
- },
753
- // Dynamic memory tools (implement-memory-system)
754
- ...dynamicMemoryTools.map((mt) => {
755
- let inputSchema;
756
- if (mt.type === "load") {
757
- inputSchema = {
758
- type: "object",
759
- properties: {
760
- query: {
761
- type: "string",
762
- description: "Optional: search query to filter agent memories. If not provided, returns recent memories."
763
- }
764
- },
765
- required: []
766
- };
767
- } else if (mt.type === "load_extreme") {
768
- inputSchema = {
769
- type: "object",
770
- properties: {
771
- query: {
772
- type: "string",
773
- description: "Optional: search query to filter agent memories. If not provided, returns recent memories."
774
- }
775
- },
776
- required: []
777
- };
778
- } else if (mt.type === "add") {
779
- inputSchema = {
780
- type: "object",
781
- properties: {
782
- content: {
783
- type: "string",
784
- description: "Memory content to record. Be concise and focused on key decisions, patterns, or context."
785
- }
786
- },
787
- required: ["content"]
788
- };
789
- } else if (mt.type === "list_all") {
790
- inputSchema = {
791
- type: "object",
792
- properties: {
793
- typeFilter: {
794
- type: "string",
795
- enum: ["all", "user", "agent"],
796
- description: "Optional: Filter by memory type. 'user' = rules, 'agent' = history. Default: 'all'"
797
- }
798
- },
799
- required: []
800
- };
801
- } else {
802
- inputSchema = {
803
- type: "object",
804
- properties: {},
805
- required: []
806
- };
807
- }
808
- return {
809
- name: mt.name,
810
- description: mt.description,
811
- inputSchema
812
- };
813
- }),
814
- // Dynamic per-prompt tools (each prompt as its own global tool)
815
- ...dynamicPromptTools.map((pt) => ({
816
- name: pt.name,
817
- description: pt.description,
818
- inputSchema: {
819
- type: "object",
820
- properties: {},
821
- required: []
822
- }
823
- }))
824
- ];
825
- return { tools };
826
- });
827
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
828
- const toolName = request.params.name;
829
- if (toolName === "prompts_run") {
830
- const parsedArgs = parsePromptsRunArgs(request.params.arguments);
831
- if (!parsedArgs) {
832
- return {
833
- content: [
834
- {
835
- type: "text",
836
- text: `Error: Missing 'slug' parameter. Provide prompt slug (e.g., 'code-review', 'plan').
837
-
838
- Use \`prompts_list\` to list available prompts.`
839
- }
840
- ],
841
- isError: true
842
- };
932
+ ...buildAuthArgs(config),
933
+ projectSlug,
934
+ ticketSlug,
935
+ content
843
936
  }
844
- const { slug: promptSlug } = parsedArgs;
845
- try {
846
- const result = await fetchAndExecuteAccountScopedPrompt(
847
- promptSlug,
848
- config,
849
- convexClient
850
- );
851
- const promptText = result.messages.map((msg) => msg.content.text).join("\n\n");
852
- return {
853
- content: [
854
- {
855
- type: "text",
856
- text: promptText
857
- }
858
- ]
859
- };
860
- } catch (error) {
861
- if (error instanceof AmbiguousPromptError) {
862
- const suggestionsList = error.suggestions.map((s) => ` \u2022 ${s}`).join("\n");
863
- console.error(`[MCP] ${toolName} ambiguous match:`, error.suggestions);
864
- return {
865
- content: [
866
- {
867
- type: "text",
868
- text: `Multiple prompts match "${promptSlug}". Please specify one of:
937
+ );
938
+ return {
939
+ content: [
940
+ {
941
+ type: "text",
942
+ text: `\u2705 Ticket [${result.slug}] paused and returned to open queue in project "${projectSlug}".
869
943
 
870
- ${suggestionsList}
944
+ Paused at: ${new Date(result.pausedAt).toISOString()}
871
945
 
872
- Example: \`prompts_run ${error.suggestions[0]}\``
873
- }
874
- ],
875
- isError: true
876
- };
946
+ _Ticket moved from working \u2192 open. Checkpoint content has been appended. Any agent can pick it up with \`tickets_work\`._`
877
947
  }
878
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
879
- console.error(`[MCP] ${toolName} error:`, error);
880
- return {
881
- content: [
882
- {
883
- type: "text",
884
- text: `Error executing prompt "${promptSlug}": ${errorMessage}`
885
- }
886
- ],
887
- isError: true
888
- };
889
- }
890
- }
891
- if (toolName === "prompts_list") {
892
- const { search: searchTerm } = parsePromptsListArgs(request.params.arguments);
893
- let filteredPrompts = [...accountScopedPrompts];
894
- if (searchTerm) {
895
- const lowerSearch = searchTerm.toLowerCase();
896
- filteredPrompts = filteredPrompts.filter(
897
- (p) => p.slug.toLowerCase().includes(lowerSearch) || p.description?.toLowerCase().includes(lowerSearch)
898
- );
899
- }
900
- filteredPrompts.sort((a, b) => a.slug.localeCompare(b.slug));
901
- const userPromptList = filteredPrompts.map((p) => {
902
- const desc = p.description || "No description";
903
- return `\u2022 ${p.slug}
904
- Description: ${desc}`;
905
- }).join("\n\n");
906
- const summary = searchTerm ? `Found ${filteredPrompts.length} prompt(s) matching "${searchTerm}":` : `Available prompts (${filteredPrompts.length} total):`;
948
+ ]
949
+ };
950
+ } catch (error) {
951
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
952
+ console.error(`[MCP] tickets_pause error:`, error);
953
+ return {
954
+ content: [{ type: "text", text: `Error pausing ticket: ${errorMessage}` }],
955
+ isError: true
956
+ };
957
+ }
958
+ }
959
+ async function handleTicketsSearch(args, config, convexClient, projectSlug) {
960
+ const parsedArgs = parseSearchArgs(args);
961
+ if (!parsedArgs) {
962
+ return {
963
+ content: [{ type: "text", text: `Error: Search query must be at least 3 characters` }],
964
+ isError: true
965
+ };
966
+ }
967
+ const { query, agent } = parsedArgs;
968
+ const resolvedAgent = resolveAgent(agent);
969
+ const typedClient = convexClient;
970
+ try {
971
+ const result = await typedClient.query("mcp_tickets:searchMcpTickets", {
972
+ ...buildAuthArgs(config),
973
+ projectSlug,
974
+ query,
975
+ agentId: resolvedAgent
976
+ });
977
+ const agentContext = resolvedAgent ? ` for agent "${resolvedAgent}"` : "";
978
+ if (result.length === 0) {
907
979
  return {
908
980
  content: [
909
981
  {
910
982
  type: "text",
911
- text: userPromptList ? `${summary}
912
-
913
- ${userPromptList}` : `${summary}
914
-
915
- No prompts found.`
983
+ text: `No tickets found matching "${query}"${agentContext} in project "${projectSlug}".`
916
984
  }
917
985
  ]
918
986
  };
919
987
  }
920
- if (toolName === "prompts_update") {
921
- const parsedArgs = parsePromptsUpdateArgs(request.params.arguments);
922
- if (!parsedArgs) {
923
- return {
924
- content: [
925
- {
926
- type: "text",
927
- text: `Error: Missing 'slug' parameter. Provide prompt slug to read or update.
988
+ const formattedList = result.map((t) => {
989
+ const statusBadge = t.status === "open" ? "\u{1F7E2}" : t.status === "working" ? "\u{1F7E1}" : t.status === "closed" ? "\u26AB" : "\u26AA";
990
+ const num = t.ticketNumber ? `#${t.ticketNumber}` : "";
991
+ const agentBadge = t.agentId ? ` [${t.agentId}]` : "";
992
+ const metadataHint = t.metadata && Object.keys(t.metadata).length > 0 ? ` {${Object.keys(t.metadata).length} meta}` : "";
993
+ return `${statusBadge} ${num} ${t.slug}${agentBadge}${metadataHint}
994
+ ${t.matchSnippet}`;
995
+ }).join("\n\n");
996
+ return {
997
+ content: [
998
+ {
999
+ type: "text",
1000
+ text: `Found ${result.length} ticket(s) matching "${query}"${agentContext}:
928
1001
 
929
- Usage:
930
- - Read mode: prompts_update { slug: "my-prompt" }
931
- - Write mode: prompts_update { slug: "my-prompt", content: "new content..." }`
932
- }
933
- ],
934
- isError: true
935
- };
1002
+ ${formattedList}`
1003
+ }
1004
+ ]
1005
+ };
1006
+ } catch (error) {
1007
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1008
+ console.error(`[MCP] tickets_search error:`, error);
1009
+ return {
1010
+ content: [{ type: "text", text: `Error searching tickets: ${errorMessage}` }],
1011
+ isError: true
1012
+ };
1013
+ }
1014
+ }
1015
+ async function handleTicketsGet(args, config, convexClient, projectSlug) {
1016
+ const parsedArgs = parseGetArgs(args);
1017
+ if (!parsedArgs) {
1018
+ return {
1019
+ content: [
1020
+ {
1021
+ type: "text",
1022
+ text: `Error: Missing ticketSlug parameter. Provide a ticket number (e.g., "102") or full slug.`
1023
+ }
1024
+ ],
1025
+ isError: true
1026
+ };
1027
+ }
1028
+ const { ticketSlug } = parsedArgs;
1029
+ const typedClient = convexClient;
1030
+ try {
1031
+ const result = await typedClient.query(
1032
+ "mcp_tickets:getMcpTicket",
1033
+ {
1034
+ ...buildAuthArgs(config),
1035
+ projectSlug,
1036
+ ticketSlug
936
1037
  }
937
- try {
938
- const result = await convexClient.mutation(
939
- "mcp_prompts:updateMcpPrompt",
1038
+ );
1039
+ if (!result) {
1040
+ return {
1041
+ content: [
940
1042
  {
941
- ...buildAuthArgs(config),
942
- promptSlug: parsedArgs.slug,
943
- content: parsedArgs.content
944
- }
945
- );
946
- if (!result.success) {
947
- const errorResult = result;
948
- if (errorResult.suggestions) {
949
- const suggestionsList = errorResult.suggestions.map((s) => ` \u2022 ${s}`).join("\n");
950
- return {
951
- content: [
952
- {
953
- type: "text",
954
- text: `${errorResult.error}
955
-
956
- Did you mean one of these?
957
- ${suggestionsList}`
958
- }
959
- ],
960
- isError: true
961
- };
1043
+ type: "text",
1044
+ text: `Ticket "${ticketSlug}" not found in project "${projectSlug}".`
962
1045
  }
963
- return {
964
- content: [{ type: "text", text: errorResult.error }],
965
- isError: true
966
- };
967
- }
968
- if (result.mode === "read") {
969
- const readResult = result;
970
- const matchNote = readResult.matchType !== "exact" ? `
971
- _Note: Matched via ${readResult.matchType} match_` : "";
972
- const aiTagNotice = readResult.hasAiTags ? `
973
-
974
- ## \u26A1 AI Tags Detected
975
- This ${readResult.resourceType} contains AI tags that need processing: **${readResult.aiTags?.join(", ")}**
976
-
977
- Process these tags by reading the instructions in each tag, generating appropriate content, and saving the result with the content parameter.` : "";
978
- const versionInfo = readResult.version !== void 0 ? ` (v${readResult.version})` : "";
979
- const resourceLabel = readResult.resourceType.charAt(0).toUpperCase() + readResult.resourceType.slice(1);
980
- const contextSection = readResult.context ? `
981
-
982
- ## Available Resources
983
- \`\`\`json
984
- ${JSON.stringify(readResult.context, null, 2)}
985
- \`\`\`` : "";
986
- return {
987
- content: [
988
- {
989
- type: "text",
990
- text: `# ${resourceLabel}: ${readResult.slug}${versionInfo}${matchNote}${aiTagNotice}
991
-
992
- ## Description
993
- ${readResult.description || "No description"}
994
-
995
- ## Content
996
- \`\`\`
997
- ${readResult.content}
998
- \`\`\`${contextSection}
1046
+ ]
1047
+ };
1048
+ }
1049
+ if (isAccessDenied(result)) {
1050
+ return {
1051
+ content: [{ type: "text", text: `Access denied: ${result.error}` }],
1052
+ isError: true
1053
+ };
1054
+ }
1055
+ const getResult = result;
1056
+ const statusBadge = getResult.status.toUpperCase();
1057
+ const startedInfo = getResult.startedAt ? `
1058
+ Started: ${new Date(getResult.startedAt).toISOString()}` : "";
1059
+ const closedInfo = getResult.closedAt ? `
1060
+ Closed: ${new Date(getResult.closedAt).toISOString()}` : "";
1061
+ let metadataInfo = "";
1062
+ if (getResult.metadata && Object.keys(getResult.metadata).length > 0) {
1063
+ metadataInfo = "\n\n**Metadata:**";
1064
+ for (const [key, value] of Object.entries(getResult.metadata)) {
1065
+ metadataInfo += `
1066
+ - ${key}: ${JSON.stringify(value)}`;
1067
+ }
1068
+ }
1069
+ return {
1070
+ content: [
1071
+ {
1072
+ type: "text",
1073
+ text: `# Ticket: ${getResult.slug} [${statusBadge}]${startedInfo}${closedInfo}
999
1074
 
1000
- ${readResult.editingGuidance}
1075
+ ${getResult.content}${metadataInfo}
1001
1076
 
1002
1077
  ---
1003
- _To update this ${readResult.resourceType}, call prompts_update with content parameter._`
1004
- }
1005
- ]
1006
- };
1007
- }
1008
- if (result.mode === "write") {
1009
- const writeResult = result;
1010
- const resourceLabel = writeResult.resourceType.charAt(0).toUpperCase() + writeResult.resourceType.slice(1);
1011
- const versionInfo = writeResult.version !== void 0 ? `
1012
- Version: ${writeResult.version}` : "";
1013
- const updatedInfo = writeResult.updatedAt ? `
1014
- Updated: ${writeResult.updatedAt}` : "";
1015
- return {
1016
- content: [
1017
- {
1018
- type: "text",
1019
- text: `\u2705 ${writeResult.message}
1020
-
1021
- ${resourceLabel}: ${writeResult.slug}${versionInfo}${updatedInfo}`
1022
- }
1023
- ]
1024
- };
1078
+ _Read-only inspection. Use tickets_work to start working on this ticket._`
1025
1079
  }
1026
- return {
1027
- content: [{ type: "text", text: "Unknown response format" }],
1028
- isError: true
1029
- };
1030
- } catch (error) {
1031
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1032
- console.error(`[MCP] prompts_update error:`, error);
1033
- return {
1034
- content: [
1035
- {
1036
- type: "text",
1037
- text: `Error updating prompt: ${errorMessage}`
1038
- }
1039
- ],
1040
- isError: true
1041
- };
1042
- }
1043
- }
1044
- const ticketTool = dynamicTicketTools.find((tt) => tt.name === toolName);
1045
- if (ticketTool) {
1046
- if (ticketTool.type === "work") {
1047
- const { ticketSlug, agent } = parseWorkArgs(request.params.arguments);
1048
- const resolvedAgent = resolveAgent(agent);
1049
- try {
1050
- const result = await convexClient.mutation(
1051
- "mcp_tickets:workMcpTicket",
1052
- {
1053
- ...buildAuthArgs(config),
1054
- projectSlug: ticketTool.projectSlug,
1055
- ticketSlug,
1056
- // Optional: specific ticket to work on
1057
- agentId: resolvedAgent
1058
- // Agent filtering (implement-agent-routing)
1059
- }
1060
- );
1061
- if (!result) {
1062
- const agentContext = resolvedAgent ? ` for agent "${resolvedAgent}"` : "";
1063
- const message = ticketSlug ? `Ticket "${ticketSlug}" not found or not in open/working status in project "${ticketTool.projectSlug}"${agentContext}.` : `No open tickets${agentContext} in project "${ticketTool.projectSlug}".`;
1064
- return {
1065
- content: [
1066
- {
1067
- type: "text",
1068
- text: message
1069
- }
1070
- ]
1071
- };
1072
- }
1073
- if (isAccessDenied(result)) {
1074
- return {
1075
- content: [
1076
- {
1077
- type: "text",
1078
- text: `Access denied: ${result.error}`
1079
- }
1080
- ],
1081
- isError: true
1082
- };
1080
+ ]
1081
+ };
1082
+ } catch (error) {
1083
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1084
+ console.error(`[MCP] tickets_get error:`, error);
1085
+ return {
1086
+ content: [{ type: "text", text: `Error getting ticket: ${errorMessage}` }],
1087
+ isError: true
1088
+ };
1089
+ }
1090
+ }
1091
+ async function handleTicketsList(args, config, convexClient, projectSlug) {
1092
+ const { agent } = parseListArgs(args);
1093
+ const resolvedAgent = resolveAgent(agent);
1094
+ const typedClient = convexClient;
1095
+ try {
1096
+ const result = await typedClient.query("mcp_tickets:listMcpTickets", {
1097
+ ...buildAuthArgs(config),
1098
+ projectSlug,
1099
+ agentId: resolvedAgent
1100
+ });
1101
+ const agentContext = resolvedAgent ? ` for agent "${resolvedAgent}"` : "";
1102
+ if (result.length === 0) {
1103
+ return {
1104
+ content: [
1105
+ {
1106
+ type: "text",
1107
+ text: `No active tickets${agentContext} in project "${projectSlug}". All tickets are closed or archived.`
1083
1108
  }
1084
- const workResult = result;
1085
- const startedInfo = workResult.startedAt ? `
1086
- Started: ${new Date(workResult.startedAt).toISOString()}` : "";
1087
- const pausedNotice = workResult.pausedAt ? `
1088
-
1089
- **\u26A0\uFE0F PAUSED TICKET HANDOFF**: This ticket was paused on ${new Date(workResult.pausedAt).toISOString()}. A previous agent checkpointed their progress. Review the checkpoint content below (look for "## Progress", "## Context", "## Remaining", "## Blockers" sections) and resume where they left off.
1090
- ` : "";
1091
- const statusNote = workResult.wasOpened ? `_Ticket moved to working status. ${workResult.remainingTickets} ticket(s) remaining in queue._` : `_Resuming work on this ticket. ${workResult.remainingTickets} ticket(s) in queue._`;
1092
- return {
1093
- content: [
1094
- {
1095
- type: "text",
1096
- text: `# Ticket: ${workResult.slug} [WORKING]${startedInfo}${pausedNotice}
1109
+ ]
1110
+ };
1111
+ }
1112
+ const openTickets = result.filter((t) => t.status === "open");
1113
+ const workingTickets = result.filter((t) => t.status === "working");
1114
+ const backlogTickets = result.filter((t) => t.status === "backlog");
1115
+ const formatTicketLine = (t) => {
1116
+ const num = t.ticketNumber ? `#${t.ticketNumber}` : "";
1117
+ const agentBadge = !resolvedAgent && t.agentId ? ` [${t.agentId}]` : "";
1118
+ return ` ${t.position}. ${num} ${t.slug}${agentBadge}
1119
+ ${t.preview}`;
1120
+ };
1121
+ const sections = [];
1122
+ if (openTickets.length > 0) {
1123
+ sections.push(
1124
+ `**\u{1F7E2} Open (${openTickets.length})**
1125
+ ${openTickets.map(formatTicketLine).join("\n")}`
1126
+ );
1127
+ }
1128
+ if (workingTickets.length > 0) {
1129
+ sections.push(
1130
+ `**\u{1F7E1} Working (${workingTickets.length})**
1131
+ ${workingTickets.map(formatTicketLine).join("\n")}`
1132
+ );
1133
+ }
1134
+ if (backlogTickets.length > 0) {
1135
+ sections.push(
1136
+ `**\u26AA Backlog (${backlogTickets.length})**
1137
+ ${backlogTickets.map(formatTicketLine).join("\n")}`
1138
+ );
1139
+ }
1140
+ const headerSuffix = resolvedAgent ? ` (Agent: ${resolvedAgent})` : "";
1141
+ return {
1142
+ content: [
1143
+ {
1144
+ type: "text",
1145
+ text: `# Active Queue: ${projectSlug}${headerSuffix}
1097
1146
 
1098
- ${workResult.content}
1147
+ ${result.length} ticket(s) in queue
1099
1148
 
1100
- ---
1101
- ${statusNote}`
1102
- }
1103
- ]
1104
- };
1105
- } catch (error) {
1106
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1107
- console.error(`[MCP] tickets_work error:`, error);
1108
- return {
1109
- content: [
1110
- {
1111
- type: "text",
1112
- text: `Error getting work: ${errorMessage}`
1113
- }
1114
- ],
1115
- isError: true
1116
- };
1149
+ ${sections.join("\n\n")}`
1117
1150
  }
1118
- } else if (ticketTool.type === "create") {
1119
- const parsedArgs = parseCreateArgs(request.params.arguments);
1120
- if (!parsedArgs) {
1121
- return {
1122
- content: [
1123
- {
1124
- type: "text",
1125
- text: `Error: Missing content parameter. Provide the ticket content (first line becomes the slug).`
1126
- }
1127
- ],
1128
- isError: true
1129
- };
1151
+ ]
1152
+ };
1153
+ } catch (error) {
1154
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1155
+ console.error(`[MCP] tickets_list error:`, error);
1156
+ return {
1157
+ content: [{ type: "text", text: `Error listing tickets: ${errorMessage}` }],
1158
+ isError: true
1159
+ };
1160
+ }
1161
+ }
1162
+ async function handleTicketsUpdate(args, config, convexClient, projectSlug) {
1163
+ const parsedArgs = parseUpdateArgs(args);
1164
+ if (!parsedArgs) {
1165
+ const rawArgs = args;
1166
+ const hasTicketSlug = typeof rawArgs?.ticketSlug === "string" && rawArgs.ticketSlug;
1167
+ const hasContent = typeof rawArgs?.content === "string" && rawArgs.content;
1168
+ if (!hasTicketSlug) {
1169
+ return {
1170
+ content: [
1171
+ {
1172
+ type: "text",
1173
+ text: `Error: Missing ticketSlug parameter. Provide a ticket number (e.g., "102") or full slug.`
1174
+ }
1175
+ ],
1176
+ isError: true
1177
+ };
1178
+ }
1179
+ if (!hasContent) {
1180
+ return {
1181
+ content: [
1182
+ {
1183
+ type: "text",
1184
+ text: `Error: Missing content parameter. Provide the update content to append to the ticket.`
1185
+ }
1186
+ ],
1187
+ isError: true
1188
+ };
1189
+ }
1190
+ return {
1191
+ content: [{ type: "text", text: `Error: Missing required parameters.` }],
1192
+ isError: true
1193
+ };
1194
+ }
1195
+ const { ticketSlug, content, agent } = parsedArgs;
1196
+ const typedClient = convexClient;
1197
+ try {
1198
+ const result = await typedClient.mutation("mcp_tickets:updateMcpTicket", {
1199
+ ...buildAuthArgs(config),
1200
+ projectSlug,
1201
+ ticketSlug,
1202
+ content,
1203
+ ...agent !== void 0 && { agentId: agent }
1204
+ });
1205
+ const agentInfo = result.agentId ? `
1206
+ Assigned to: ${result.agentId}` : agent === "" ? "\nAgent: Unassigned" : "";
1207
+ return {
1208
+ content: [
1209
+ {
1210
+ type: "text",
1211
+ text: `\u2705 Ticket [${result.slug}] updated in project "${projectSlug}".${agentInfo}
1212
+ Updated at: ${new Date(result.updatedAt).toISOString()}
1213
+ _Ticket content has been appended with your update._`
1130
1214
  }
1131
- const { content, agent } = parsedArgs;
1132
- const resolvedAgent = resolveAgent(agent);
1133
- try {
1134
- const result = await convexClient.mutation(
1135
- "mcp_tickets:createMcpTicket",
1136
- {
1137
- ...buildAuthArgs(config),
1138
- projectSlug: ticketTool.projectSlug,
1139
- content,
1140
- agentId: resolvedAgent
1141
- // Agent assignment (implement-agent-routing)
1142
- }
1143
- );
1144
- const agentInfo = result.agentId ? `
1145
- Assigned to: ${result.agentId}` : "";
1146
- return {
1147
- content: [
1148
- {
1149
- type: "text",
1150
- text: `\u2705 Created ticket [${result.slug}] in backlog for project "${ticketTool.projectSlug}".${agentInfo}
1215
+ ]
1216
+ };
1217
+ } catch (error) {
1218
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1219
+ console.error(`[MCP] tickets_update error:`, error);
1220
+ return {
1221
+ content: [{ type: "text", text: `Error updating ticket: ${errorMessage}` }],
1222
+ isError: true
1223
+ };
1224
+ }
1225
+ }
1151
1226
 
1152
- Position: #${result.position} of ${result.totalBacklog} backlog tickets
1153
- Preview: ${result.preview}
1227
+ // src/handlers/memories.ts
1228
+ async function handleMemoriesLoad(args, config, convexClient, projectSlug) {
1229
+ const { query } = parseMemoriesLoadArgs(args);
1230
+ const typedClient = convexClient;
1231
+ try {
1232
+ const result = await typedClient.query("mcp_memories:loadMemories", {
1233
+ ...buildAuthArgs(config),
1234
+ projectSlug,
1235
+ query
1236
+ });
1237
+ const sections = [];
1238
+ if (result.userMemories.length > 0) {
1239
+ const rulesText = result.userMemories.map((m, i) => `${i + 1}. ${m.content}`).join("\n");
1240
+ sections.push(`## Rules (${result.userMemories.length})
1241
+ ${rulesText}`);
1242
+ } else {
1243
+ sections.push(`## Rules
1244
+ _No rules defined. Create rules in the Memories page._`);
1245
+ }
1246
+ if (result.agentMemories.length > 0) {
1247
+ const historyText = result.agentMemories.map((m) => {
1248
+ const ticketRef = m.ticketNumber ? `[#${m.ticketNumber}]` : "";
1249
+ const date = new Date(m.createdAt).toISOString().split("T")[0];
1250
+ return `\u2022 ${ticketRef} ${m.content} _(${date})_`;
1251
+ }).join("\n");
1252
+ const searchNote = query ? ` matching "${query}"` : " (recent)";
1253
+ sections.push(`## History${searchNote} (${result.agentMemories.length})
1254
+ ${historyText}`);
1255
+ } else {
1256
+ const searchNote = query ? ` matching "${query}"` : "";
1257
+ sections.push(`## History${searchNote}
1258
+ _No memories recorded yet._`);
1259
+ }
1260
+ return {
1261
+ content: [
1262
+ {
1263
+ type: "text",
1264
+ text: `# Project Memories: ${projectSlug}
1154
1265
 
1155
- _Ticket created in backlog. Use \`tickets_work ${result.slug}\` to move it to working status._`
1156
- }
1157
- ]
1158
- };
1159
- } catch (error) {
1160
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1161
- console.error(`[MCP] tickets_create error:`, error);
1162
- return {
1163
- content: [
1164
- {
1165
- type: "text",
1166
- text: `Error creating ticket: ${errorMessage}`
1167
- }
1168
- ],
1169
- isError: true
1170
- };
1266
+ ${sections.join("\n\n")}`
1171
1267
  }
1172
- } else if (ticketTool.type === "close") {
1173
- const parsedArgs = parseCloseArgs(request.params.arguments);
1174
- if (!parsedArgs) {
1175
- return {
1176
- content: [
1177
- {
1178
- type: "text",
1179
- text: `Error: Missing ticketSlug parameter. Usage: Provide a ticket number (e.g., "102") or full slug (e.g., "102-fix-auth").`
1180
- }
1181
- ],
1182
- isError: true
1183
- };
1268
+ ]
1269
+ };
1270
+ } catch (error) {
1271
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1272
+ console.error(`[MCP] memories_load error:`, error);
1273
+ return {
1274
+ content: [{ type: "text", text: `Error loading memories: ${errorMessage}` }],
1275
+ isError: true
1276
+ };
1277
+ }
1278
+ }
1279
+ async function handleMemoriesLoadExtreme(args, config, convexClient, projectSlug) {
1280
+ const { query } = parseMemoriesLoadArgs(args);
1281
+ const typedClient = convexClient;
1282
+ try {
1283
+ const result = await typedClient.query(
1284
+ "mcp_memories:loadMemoriesExtreme",
1285
+ {
1286
+ ...buildAuthArgs(config),
1287
+ projectSlug,
1288
+ query
1289
+ }
1290
+ );
1291
+ const lines = [];
1292
+ lines.push(`# Project Memories: ${projectSlug}
1293
+ `);
1294
+ lines.push(`## Rules (${result.userMemories.length})`);
1295
+ if (result.userMemories.length > 0) {
1296
+ result.userMemories.forEach((m, i) => {
1297
+ lines.push(`${i + 1}. ${m.content}`);
1298
+ });
1299
+ } else {
1300
+ lines.push(`_No rules defined. Create rules in the Memories page._`);
1301
+ }
1302
+ lines.push("");
1303
+ const historyHeader = query ? `## History matching "${query}" (${result.agentMemories.length})` : `## Recent History (${result.agentMemories.length})`;
1304
+ lines.push(historyHeader);
1305
+ lines.push("");
1306
+ for (const memory of result.agentMemories) {
1307
+ lines.push("---");
1308
+ lines.push(`**Memory:** ${memory.content}`);
1309
+ if (memory.ticket) {
1310
+ lines.push(`**Ticket:** #${memory.ticket.ticketNumber}-${memory.ticket.slug}`);
1311
+ }
1312
+ const date = new Date(memory.createdAt).toISOString().split("T")[0];
1313
+ lines.push(`**Date:** ${date}`);
1314
+ lines.push("");
1315
+ if (memory.ticket) {
1316
+ lines.push("<ticket-content>");
1317
+ lines.push(memory.ticket.content);
1318
+ lines.push("</ticket-content>");
1319
+ } else {
1320
+ lines.push("_(No linked ticket)_");
1321
+ }
1322
+ lines.push("");
1323
+ }
1324
+ return {
1325
+ content: [{ type: "text", text: lines.join("\n") }]
1326
+ };
1327
+ } catch (error) {
1328
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1329
+ console.error(`[MCP] memories_load_extreme error:`, error);
1330
+ return {
1331
+ content: [{ type: "text", text: `Error loading extreme memories: ${errorMessage}` }],
1332
+ isError: true
1333
+ };
1334
+ }
1335
+ }
1336
+ async function handleMemoriesAdd(args, config, convexClient, projectSlug) {
1337
+ const parsedArgs = parseMemoriesAddArgs(args);
1338
+ if (!parsedArgs) {
1339
+ return {
1340
+ content: [
1341
+ {
1342
+ type: "text",
1343
+ text: `Error: Missing content parameter. Provide the memory content to record.`
1184
1344
  }
1185
- const { ticketSlug, metadata } = parsedArgs;
1186
- try {
1187
- const result = await convexClient.mutation(
1188
- "mcp_tickets:closeMcpTicket",
1189
- {
1190
- ...buildAuthArgs(config),
1191
- projectSlug: ticketTool.projectSlug,
1192
- ticketSlug,
1193
- metadata
1194
- }
1195
- );
1196
- let responseText = `\u2705 Ticket [${result.slug}] closed in project "${ticketTool.projectSlug}".
1345
+ ],
1346
+ isError: true
1347
+ };
1348
+ }
1349
+ const { content } = parsedArgs;
1350
+ const typedClient = convexClient;
1351
+ try {
1352
+ const result = await typedClient.mutation("mcp_memories:addMemory", {
1353
+ ...buildAuthArgs(config),
1354
+ projectSlug,
1355
+ content
1356
+ });
1357
+ let responseText;
1358
+ if (result.linkedTicket) {
1359
+ const ticketRef = `#${result.linkedTicket.number}`;
1360
+ const fallbackNote = result.linkedViaFallback ? " (recently closed)" : "";
1361
+ responseText = `\u2705 Memory recorded for ticket [${ticketRef}]${fallbackNote} in project "${projectSlug}".
1197
1362
 
1198
- Closed at: ${new Date(result.closedAt).toISOString()}`;
1199
- if (result.metadata && Object.keys(result.metadata).length > 0) {
1200
- responseText += `
1363
+ _Memory will be available in future context via [MEMORY_LOAD]._`;
1364
+ } else {
1365
+ responseText = `\u2705 Memory recorded in project "${projectSlug}".
1201
1366
 
1202
- **Metadata saved:**`;
1203
- for (const [key, value] of Object.entries(result.metadata)) {
1204
- responseText += `
1205
- - ${key}: ${JSON.stringify(value)}`;
1206
- }
1207
- }
1208
- responseText += `
1367
+ _Memory created without ticket linkage (no working ticket or recent closed ticket)._
1368
+ _Memory will be available in future context via [MEMORY_LOAD]._`;
1369
+ }
1370
+ return {
1371
+ content: [{ type: "text", text: responseText }]
1372
+ };
1373
+ } catch (error) {
1374
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1375
+ console.error(`[MCP] memories_add error:`, error);
1376
+ return {
1377
+ content: [{ type: "text", text: `Error adding memory: ${errorMessage}` }],
1378
+ isError: true
1379
+ };
1380
+ }
1381
+ }
1382
+ async function handleMemoriesListAll(args, config, convexClient, projectSlug) {
1383
+ const parsedArgs = parseMemoriesListAllArgs(args);
1384
+ const typedClient = convexClient;
1385
+ try {
1386
+ const result = await typedClient.query("mcp_memories:listAllMemories", {
1387
+ ...buildAuthArgs(config),
1388
+ projectSlug,
1389
+ typeFilter: parsedArgs.typeFilter
1390
+ });
1391
+ const formatDate = (ts) => new Date(ts).toISOString().split("T")[0];
1392
+ const userMemories = result.memories.filter((m) => m.type === "user");
1393
+ const agentMemories = result.memories.filter((m) => m.type === "agent");
1394
+ let output = `# Project Memories (${result.totalCount} total)
1209
1395
 
1210
- _Reminder: Ensure all embedded \`[RUN_PROMPT ...]\` directives were executed before closing._`;
1211
- return {
1212
- content: [
1213
- {
1214
- type: "text",
1215
- text: responseText
1216
- }
1217
- ]
1218
- };
1219
- } catch (error) {
1220
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1221
- console.error(`[MCP] tickets_close error:`, error);
1222
- return {
1223
- content: [
1224
- {
1225
- type: "text",
1226
- text: `Error closing ticket: ${errorMessage}`
1227
- }
1228
- ],
1229
- isError: true
1230
- };
1231
- }
1232
- } else if (ticketTool.type === "pause") {
1233
- const parsedArgs = parsePauseArgs(request.params.arguments);
1234
- if (!parsedArgs) {
1235
- const args = request.params.arguments;
1236
- const hasTicketSlug = typeof args?.ticketSlug === "string" && args.ticketSlug;
1237
- const hasContent = typeof args?.content === "string" && args.content;
1238
- if (!hasTicketSlug) {
1239
- return {
1240
- content: [
1241
- {
1242
- type: "text",
1243
- text: `Error: Missing ticketSlug parameter. Provide a ticket number (e.g., "102") or full slug.`
1244
- }
1245
- ],
1246
- isError: true
1247
- };
1248
- }
1249
- if (!hasContent) {
1250
- return {
1251
- content: [
1252
- {
1253
- type: "text",
1254
- text: `Error: Missing content parameter. Describe what's done, what remains, and any blockers.`
1255
- }
1256
- ],
1257
- isError: true
1258
- };
1259
- }
1260
- return {
1261
- content: [
1262
- {
1263
- type: "text",
1264
- text: `Error: Missing required parameters.`
1265
- }
1266
- ],
1267
- isError: true
1268
- };
1269
- }
1270
- const { ticketSlug, content } = parsedArgs;
1271
- try {
1272
- const result = await convexClient.mutation(
1273
- "mcp_tickets:pauseMcpTicket",
1274
- {
1275
- ...buildAuthArgs(config),
1276
- projectSlug: ticketTool.projectSlug,
1277
- ticketSlug,
1278
- content
1279
- }
1280
- );
1281
- return {
1282
- content: [
1283
- {
1284
- type: "text",
1285
- text: `\u2705 Ticket [${result.slug}] paused and returned to open queue in project "${ticketTool.projectSlug}".
1396
+ `;
1397
+ if (userMemories.length > 0) {
1398
+ output += `## Rules (${userMemories.length})
1399
+ `;
1400
+ userMemories.forEach((m, i) => {
1401
+ output += `${i + 1}. [${m.id}] ${m.content}
1402
+ `;
1403
+ });
1404
+ output += "\n";
1405
+ }
1406
+ if (agentMemories.length > 0) {
1407
+ output += `## Agent History (${agentMemories.length})
1408
+ `;
1409
+ agentMemories.forEach((m) => {
1410
+ const ticketRef = m.ticketNumber ? ` [#${m.ticketNumber}]` : "";
1411
+ output += `\u2022 [${m.id}]${ticketRef} ${m.content.substring(0, 100)}${m.content.length > 100 ? "..." : ""} _(${formatDate(m.createdAt)})_
1412
+ `;
1413
+ });
1414
+ }
1415
+ return {
1416
+ content: [{ type: "text", text: output }]
1417
+ };
1418
+ } catch (error) {
1419
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1420
+ console.error(`[MCP] memories_list_all error:`, error);
1421
+ return {
1422
+ content: [{ type: "text", text: `Error listing memories: ${errorMessage}` }],
1423
+ isError: true
1424
+ };
1425
+ }
1426
+ }
1427
+
1428
+ // src/server.ts
1429
+ async function startServer(config, convexClient) {
1430
+ console.error("[MCP] Validating project token...");
1431
+ const validation = await validateProjectToken(convexClient, config.projectToken);
1432
+ if (!validation.valid) {
1433
+ throw new Error(`Invalid project token: ${validation.error}`);
1434
+ }
1435
+ const projectSlug = validation.projectSlug;
1436
+ console.error(`[MCP] Project token validated for project "${projectSlug}"`);
1437
+ console.error("[MCP] Fetching prompt metadata...");
1438
+ const accountScopedPrompts = await fetchAccountScopedPromptMetadataByToken(
1439
+ convexClient,
1440
+ config.projectToken
1441
+ );
1442
+ console.error(`[MCP] Project token mode: loaded ${accountScopedPrompts.length} prompts`);
1443
+ const slashCommandPrompts = accountScopedPrompts.filter((p) => p.slashCommand === true);
1444
+ console.error(`[MCP] ${slashCommandPrompts.length} prompts registered as slash commands`);
1445
+ if (accountScopedPrompts.length === 0) {
1446
+ console.error(
1447
+ "[MCP] WARNING: No prompts found. Create prompts in the 'prompts' section to expose them via MCP."
1448
+ );
1449
+ }
1450
+ const ticketTools = getTicketToolDefinitions(projectSlug);
1451
+ const memoryTools = getMemoryToolDefinitions();
1452
+ const promptTools = getPromptToolDefinitions();
1453
+ console.error(`[MCP] Registering ${ticketTools.length} ticket tools...`);
1454
+ console.error(`[MCP] Registering ${memoryTools.length} memory tools...`);
1455
+ console.error(`[MCP] Registering ${promptTools.length} global prompt tools...`);
1456
+ const dynamicPromptTools = accountScopedPrompts.map((prompt) => {
1457
+ const folderPrefix = prompt.folderPath ? `[${prompt.folderPath}] ` : "";
1458
+ const baseDescription = prompt.description || `Execute the "${prompt.slug}" prompt`;
1459
+ return {
1460
+ name: prompt.slug,
1461
+ description: folderPrefix + baseDescription,
1462
+ promptSlug: prompt.slug
1463
+ };
1464
+ });
1465
+ console.error(`[MCP] Registering ${dynamicPromptTools.length} per-prompt tools (global)...`);
1466
+ const promptNames = /* @__PURE__ */ new Set();
1467
+ const duplicates = [];
1468
+ accountScopedPrompts.forEach((p) => {
1469
+ if (promptNames.has(p.slug)) {
1470
+ duplicates.push(p.slug);
1471
+ }
1472
+ promptNames.add(p.slug);
1473
+ });
1474
+ if (duplicates.length > 0) {
1475
+ console.error(
1476
+ `[MCP] WARNING: Duplicate prompt slugs detected: ${duplicates.join(", ")}. Only the first occurrence will be registered.`
1477
+ );
1478
+ }
1479
+ const server = new Server(
1480
+ { name: "ppm-mcp-server", version: "3.1.0" },
1481
+ { capabilities: { prompts: {}, tools: {} } }
1482
+ );
1483
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
1484
+ return {
1485
+ prompts: [
1486
+ // Global prompt tools (prompts_run, prompts_list, prompts_update)
1487
+ ...promptTools.map((t) => ({ name: t.name, description: t.description })),
1488
+ // Ticket tools
1489
+ ...ticketTools.map((t) => ({ name: t.name, description: t.description })),
1490
+ // Memory tools
1491
+ ...memoryTools.map((t) => ({ name: t.name, description: t.description })),
1492
+ // User prompts with slashCommand: true
1493
+ ...slashCommandPrompts.map((p) => ({
1494
+ name: p.slug,
1495
+ description: p.description || `Execute the "${p.slug}" prompt`
1496
+ }))
1497
+ ]
1498
+ };
1499
+ });
1500
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
1501
+ const promptName = request.params.name;
1502
+ const ticketWorkMatch = promptName.match(/^tickets_work\s+(.+)$/);
1503
+ if (ticketWorkMatch) {
1504
+ const ticketArg = ticketWorkMatch[1].trim();
1505
+ return {
1506
+ description: `Work on ticket "${ticketArg}" from "${projectSlug}"`,
1507
+ messages: [
1508
+ {
1509
+ role: "user",
1510
+ content: {
1511
+ type: "text",
1512
+ text: `Get work on ticket "${ticketArg}" from the "${projectSlug}" project.
1286
1513
 
1287
- Paused at: ${new Date(result.pausedAt).toISOString()}
1514
+ Call the \`tickets_work\` tool with ticketSlug: "${ticketArg}".
1288
1515
 
1289
- _Ticket moved from working \u2192 open. Checkpoint content has been appended. Any agent can pick it up with \`tickets_work\`._`
1290
- }
1291
- ]
1292
- };
1293
- } catch (error) {
1294
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1295
- console.error(`[MCP] tickets_pause error:`, error);
1296
- return {
1297
- content: [
1298
- {
1299
- type: "text",
1300
- text: `Error pausing ticket: ${errorMessage}`
1301
- }
1302
- ],
1303
- isError: true
1304
- };
1305
- }
1306
- } else if (ticketTool.type === "search") {
1307
- const parsedArgs = parseSearchArgs(request.params.arguments);
1308
- if (!parsedArgs) {
1309
- return {
1310
- content: [
1311
- {
1312
- type: "text",
1313
- text: `Error: Search query must be at least 3 characters`
1314
- }
1315
- ],
1316
- isError: true
1317
- };
1318
- }
1319
- const { query, agent } = parsedArgs;
1320
- const resolvedAgent = resolveAgent(agent);
1321
- try {
1322
- const result = await convexClient.query(
1323
- "mcp_tickets:searchMcpTickets",
1324
- {
1325
- ...buildAuthArgs(config),
1326
- projectSlug: ticketTool.projectSlug,
1327
- query,
1328
- agentId: resolvedAgent
1329
- // Agent filtering (implement-agent-routing)
1516
+ This will open the ticket if it's in the open queue, or resume it if already working.`
1330
1517
  }
1331
- );
1332
- const agentContext = resolvedAgent ? ` for agent "${resolvedAgent}"` : "";
1333
- if (result.length === 0) {
1334
- return {
1335
- content: [
1336
- {
1337
- type: "text",
1338
- text: `No tickets found matching "${query}"${agentContext} in project "${ticketTool.projectSlug}".`
1339
- }
1340
- ]
1341
- };
1342
1518
  }
1343
- const formattedList = result.map((t) => {
1344
- const statusBadge = t.status === "open" ? "\u{1F7E2}" : t.status === "working" ? "\u{1F7E1}" : t.status === "closed" ? "\u26AB" : "\u26AA";
1345
- const num = t.ticketNumber ? `#${t.ticketNumber}` : "";
1346
- const agentBadge = t.agentId ? ` [${t.agentId}]` : "";
1347
- const metadataHint = t.metadata && Object.keys(t.metadata).length > 0 ? ` {${Object.keys(t.metadata).length} meta}` : "";
1348
- return `${statusBadge} ${num} ${t.slug}${agentBadge}${metadataHint}
1349
- ${t.matchSnippet}`;
1350
- }).join("\n\n");
1351
- return {
1352
- content: [
1353
- {
1354
- type: "text",
1355
- text: `Found ${result.length} ticket(s) matching "${query}"${agentContext}:
1519
+ ]
1520
+ };
1521
+ }
1522
+ const ticketTool = ticketTools.find((t) => t.name === promptName);
1523
+ if (ticketTool) {
1524
+ return buildTicketPromptResponse(ticketTool.type, ticketTool.name, projectSlug);
1525
+ }
1526
+ if (promptName === "prompts_list") {
1527
+ return {
1528
+ description: "List all available prompts",
1529
+ messages: [
1530
+ {
1531
+ role: "user",
1532
+ content: {
1533
+ type: "text",
1534
+ text: `List all available prompts.
1356
1535
 
1357
- ${formattedList}`
1358
- }
1359
- ]
1360
- };
1361
- } catch (error) {
1362
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1363
- console.error(`[MCP] tickets_search error:`, error);
1364
- return {
1365
- content: [
1366
- {
1367
- type: "text",
1368
- text: `Error searching tickets: ${errorMessage}`
1369
- }
1370
- ],
1371
- isError: true
1372
- };
1373
- }
1374
- } else if (ticketTool.type === "get") {
1375
- const parsedArgs = parseGetArgs(request.params.arguments);
1376
- if (!parsedArgs) {
1377
- return {
1378
- content: [
1379
- {
1380
- type: "text",
1381
- text: `Error: Missing ticketSlug parameter. Provide a ticket number (e.g., "102") or full slug.`
1382
- }
1383
- ],
1384
- isError: true
1385
- };
1386
- }
1387
- const { ticketSlug } = parsedArgs;
1388
- try {
1389
- const result = await convexClient.query(
1390
- "mcp_tickets:getMcpTicket",
1391
- {
1392
- ...buildAuthArgs(config),
1393
- projectSlug: ticketTool.projectSlug,
1394
- ticketSlug
1395
- }
1396
- );
1397
- if (!result) {
1398
- return {
1399
- content: [
1400
- {
1401
- type: "text",
1402
- text: `Ticket "${ticketSlug}" not found in project "${ticketTool.projectSlug}".`
1403
- }
1404
- ]
1405
- };
1406
- }
1407
- if (isAccessDenied(result)) {
1408
- return {
1409
- content: [
1410
- {
1411
- type: "text",
1412
- text: `Access denied: ${result.error}`
1413
- }
1414
- ],
1415
- isError: true
1416
- };
1417
- }
1418
- const getResult = result;
1419
- const statusBadge = getResult.status.toUpperCase();
1420
- const startedInfo = getResult.startedAt ? `
1421
- Started: ${new Date(getResult.startedAt).toISOString()}` : "";
1422
- const closedInfo = getResult.closedAt ? `
1423
- Closed: ${new Date(getResult.closedAt).toISOString()}` : "";
1424
- let metadataInfo = "";
1425
- if (getResult.metadata && Object.keys(getResult.metadata).length > 0) {
1426
- metadataInfo = "\n\n**Metadata:**";
1427
- for (const [key, value] of Object.entries(getResult.metadata)) {
1428
- metadataInfo += `
1429
- - ${key}: ${JSON.stringify(value)}`;
1536
+ Call the \`prompts_list\` tool with optional \`search\` parameter to filter results.`
1430
1537
  }
1431
1538
  }
1432
- return {
1433
- content: [
1434
- {
1435
- type: "text",
1436
- text: `# Ticket: ${getResult.slug} [${statusBadge}]${startedInfo}${closedInfo}
1437
-
1438
- ${getResult.content}${metadataInfo}
1439
-
1440
- ---
1441
- _Read-only inspection. Use tickets_work to start working on this ticket._`
1442
- }
1443
- ]
1444
- };
1445
- } catch (error) {
1446
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1447
- console.error(`[MCP] tickets_get error:`, error);
1448
- return {
1449
- content: [
1450
- {
1451
- type: "text",
1452
- text: `Error getting ticket: ${errorMessage}`
1453
- }
1454
- ],
1455
- isError: true
1456
- };
1457
- }
1458
- } else if (ticketTool.type === "list") {
1459
- const { agent } = parseListArgs(request.params.arguments);
1460
- const resolvedAgent = resolveAgent(agent);
1461
- try {
1462
- const result = await convexClient.query(
1463
- "mcp_tickets:listMcpTickets",
1539
+ ]
1540
+ };
1541
+ }
1542
+ const runPromptMatch = promptName.match(/^prompts_run\s+(.+)$/);
1543
+ if (promptName === "prompts_run" || runPromptMatch) {
1544
+ if (runPromptMatch) {
1545
+ const promptSlug = runPromptMatch[1].trim();
1546
+ return {
1547
+ description: `Execute prompt "${promptSlug}"`,
1548
+ messages: [
1464
1549
  {
1465
- ...buildAuthArgs(config),
1466
- projectSlug: ticketTool.projectSlug,
1467
- agentId: resolvedAgent
1468
- // Agent filtering (implement-agent-routing)
1469
- }
1470
- );
1471
- const agentContext = resolvedAgent ? ` for agent "${resolvedAgent}"` : "";
1472
- if (result.length === 0) {
1473
- return {
1474
- content: [
1475
- {
1476
- type: "text",
1477
- text: `No active tickets${agentContext} in project "${ticketTool.projectSlug}". All tickets are closed or archived.`
1478
- }
1479
- ]
1480
- };
1481
- }
1482
- const openTickets = result.filter((t) => t.status === "open");
1483
- const workingTickets = result.filter((t) => t.status === "working");
1484
- const backlogTickets = result.filter((t) => t.status === "backlog");
1485
- const formatTicketLine = (t) => {
1486
- const num = t.ticketNumber ? `#${t.ticketNumber}` : "";
1487
- const agentBadge = !resolvedAgent && t.agentId ? ` [${t.agentId}]` : "";
1488
- return ` ${t.position}. ${num} ${t.slug}${agentBadge}
1489
- ${t.preview}`;
1490
- };
1491
- const sections = [];
1492
- if (openTickets.length > 0) {
1493
- sections.push(`**\u{1F7E2} Open (${openTickets.length})**
1494
- ${openTickets.map(formatTicketLine).join("\n")}`);
1495
- }
1496
- if (workingTickets.length > 0) {
1497
- sections.push(`**\u{1F7E1} Working (${workingTickets.length})**
1498
- ${workingTickets.map(formatTicketLine).join("\n")}`);
1499
- }
1500
- if (backlogTickets.length > 0) {
1501
- sections.push(`**\u26AA Backlog (${backlogTickets.length})**
1502
- ${backlogTickets.map(formatTicketLine).join("\n")}`);
1503
- }
1504
- const headerSuffix = resolvedAgent ? ` (Agent: ${resolvedAgent})` : "";
1505
- return {
1506
- content: [
1507
- {
1550
+ role: "user",
1551
+ content: {
1508
1552
  type: "text",
1509
- text: `# Active Queue: ${ticketTool.projectSlug}${headerSuffix}
1553
+ text: `Execute the "${promptSlug}" prompt.
1510
1554
 
1511
- ${result.length} ticket(s) in queue
1512
-
1513
- ${sections.join("\n\n")}`
1514
- }
1515
- ]
1516
- };
1517
- } catch (error) {
1518
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1519
- console.error(`[MCP] tickets_list error:`, error);
1520
- return {
1521
- content: [
1522
- {
1523
- type: "text",
1524
- text: `Error listing tickets: ${errorMessage}`
1525
- }
1526
- ],
1527
- isError: true
1528
- };
1529
- }
1530
- } else if (ticketTool.type === "update") {
1531
- const parsedArgs = parseUpdateArgs(request.params.arguments);
1532
- if (!parsedArgs) {
1533
- const args = request.params.arguments;
1534
- const hasTicketSlug = typeof args?.ticketSlug === "string" && args.ticketSlug;
1535
- const hasContent = typeof args?.content === "string" && args.content;
1536
- if (!hasTicketSlug) {
1537
- return {
1538
- content: [
1539
- {
1540
- type: "text",
1541
- text: `Error: Missing ticketSlug parameter. Provide a ticket number (e.g., "102") or full slug.`
1542
- }
1543
- ],
1544
- isError: true
1545
- };
1546
- }
1547
- if (!hasContent) {
1548
- return {
1549
- content: [
1550
- {
1551
- type: "text",
1552
- text: `Error: Missing content parameter. Provide the update content to append to the ticket.`
1553
- }
1554
- ],
1555
- isError: true
1556
- };
1557
- }
1558
- return {
1559
- content: [
1560
- {
1561
- type: "text",
1562
- text: `Error: Missing required parameters.`
1555
+ Call the \`prompts_run\` tool with slug: "${promptSlug}".`
1563
1556
  }
1564
- ],
1565
- isError: true
1566
- };
1567
- }
1568
- const { ticketSlug, content, agent } = parsedArgs;
1569
- try {
1570
- const result = await convexClient.mutation(
1571
- "mcp_tickets:updateMcpTicket",
1572
- {
1573
- ...buildAuthArgs(config),
1574
- projectSlug: ticketTool.projectSlug,
1575
- ticketSlug,
1576
- content,
1577
- // Agent reassignment (implement-agent-routing)
1578
- // Only include if explicitly provided (empty string = unassign)
1579
- ...agent !== void 0 && { agentId: agent }
1580
1557
  }
1581
- );
1582
- const agentInfo = result.agentId ? `
1583
- Assigned to: ${result.agentId}` : agent === "" ? "\nAgent: Unassigned" : "";
1584
- return {
1585
- content: [
1586
- {
1587
- type: "text",
1588
- text: `\u2705 Ticket [${result.slug}] updated in project "${ticketTool.projectSlug}".${agentInfo}
1589
- Updated at: ${new Date(result.updatedAt).toISOString()}
1590
- _Ticket content has been appended with your update._`
1591
- }
1592
- ]
1593
- };
1594
- } catch (error) {
1595
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1596
- console.error(`[MCP] tickets_update error:`, error);
1597
- return {
1598
- content: [
1599
- {
1600
- type: "text",
1601
- text: `Error updating ticket: ${errorMessage}`
1602
- }
1603
- ],
1604
- isError: true
1605
- };
1606
- }
1558
+ ]
1559
+ };
1607
1560
  }
1608
- }
1609
- const memoryTool = dynamicMemoryTools.find((mt) => mt.name === toolName);
1610
- if (memoryTool) {
1611
- if (memoryTool.type === "load") {
1612
- const { query } = parseMemoriesLoadArgs(request.params.arguments);
1613
- try {
1614
- const result = await convexClient.query(
1615
- "mcp_memories:loadMemories",
1616
- {
1617
- ...buildAuthArgs(config),
1618
- projectSlug: memoryTool.projectSlug,
1619
- query
1620
- }
1621
- );
1622
- const sections = [];
1623
- if (result.userMemories.length > 0) {
1624
- const rulesText = result.userMemories.map((m, i) => `${i + 1}. ${m.content}`).join("\n");
1625
- sections.push(`## Rules (${result.userMemories.length})
1626
- ${rulesText}`);
1627
- } else {
1628
- sections.push(`## Rules
1629
- _No rules defined. Create rules in the Memories page._`);
1630
- }
1631
- if (result.agentMemories.length > 0) {
1632
- const historyText = result.agentMemories.map((m) => {
1633
- const ticketRef = m.ticketNumber ? `[#${m.ticketNumber}]` : "";
1634
- const date = new Date(m.createdAt).toISOString().split("T")[0];
1635
- return `\u2022 ${ticketRef} ${m.content} _(${date})_`;
1636
- }).join("\n");
1637
- const searchNote = query ? ` matching "${query}"` : " (recent)";
1638
- sections.push(`## History${searchNote} (${result.agentMemories.length})
1639
- ${historyText}`);
1640
- } else {
1641
- const searchNote = query ? ` matching "${query}"` : "";
1642
- sections.push(`## History${searchNote}
1643
- _No memories recorded yet._`);
1644
- }
1645
- return {
1646
- content: [
1647
- {
1648
- type: "text",
1649
- text: `# Project Memories: ${memoryTool.projectSlug}
1561
+ return {
1562
+ description: "Execute a prompt by slug",
1563
+ messages: [
1564
+ {
1565
+ role: "user",
1566
+ content: {
1567
+ type: "text",
1568
+ text: `Execute a prompt by slug.
1650
1569
 
1651
- ${sections.join("\n\n")}`
1652
- }
1653
- ]
1654
- };
1655
- } catch (error) {
1656
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1657
- console.error(`[MCP] memories_load error:`, error);
1658
- return {
1659
- content: [
1660
- {
1661
- type: "text",
1662
- text: `Error loading memories: ${errorMessage}`
1663
- }
1664
- ],
1665
- isError: true
1666
- };
1667
- }
1668
- } else if (memoryTool.type === "load_extreme") {
1669
- const { query } = parseMemoriesLoadArgs(request.params.arguments);
1670
- try {
1671
- const result = await convexClient.query(
1672
- "mcp_memories:loadMemoriesExtreme",
1673
- {
1674
- ...buildAuthArgs(config),
1675
- projectSlug: memoryTool.projectSlug,
1676
- query
1677
- }
1678
- );
1679
- const lines = [];
1680
- lines.push(`# Project Memories: ${memoryTool.projectSlug}
1681
- `);
1682
- lines.push(`## Rules (${result.userMemories.length})`);
1683
- if (result.userMemories.length > 0) {
1684
- result.userMemories.forEach((m, i) => {
1685
- lines.push(`${i + 1}. ${m.content}`);
1686
- });
1687
- } else {
1688
- lines.push(`_No rules defined. Create rules in the Memories page._`);
1689
- }
1690
- lines.push("");
1691
- const historyHeader = query ? `## History matching "${query}" (${result.agentMemories.length})` : `## Recent History (${result.agentMemories.length})`;
1692
- lines.push(historyHeader);
1693
- lines.push("");
1694
- for (const memory of result.agentMemories) {
1695
- lines.push("---");
1696
- lines.push(`**Memory:** ${memory.content}`);
1697
- if (memory.ticket) {
1698
- lines.push(`**Ticket:** #${memory.ticket.ticketNumber}-${memory.ticket.slug}`);
1699
- }
1700
- const date = new Date(memory.createdAt).toISOString().split("T")[0];
1701
- lines.push(`**Date:** ${date}`);
1702
- lines.push("");
1703
- if (memory.ticket) {
1704
- lines.push("<ticket-content>");
1705
- lines.push(memory.ticket.content);
1706
- lines.push("</ticket-content>");
1707
- } else {
1708
- lines.push("_(No linked ticket)_");
1709
- }
1710
- lines.push("");
1711
- }
1712
- return {
1713
- content: [
1714
- {
1715
- type: "text",
1716
- text: lines.join("\n")
1717
- }
1718
- ]
1719
- };
1720
- } catch (error) {
1721
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1722
- console.error(`[MCP] memories_load_extreme error:`, error);
1723
- return {
1724
- content: [
1725
- {
1726
- type: "text",
1727
- text: `Error loading extreme memories: ${errorMessage}`
1728
- }
1729
- ],
1730
- isError: true
1731
- };
1732
- }
1733
- } else if (memoryTool.type === "add") {
1734
- const parsedArgs = parseMemoriesAddArgs(request.params.arguments);
1735
- if (!parsedArgs) {
1736
- return {
1737
- content: [
1738
- {
1739
- type: "text",
1740
- text: `Error: Missing content parameter. Provide the memory content to record.`
1741
- }
1742
- ],
1743
- isError: true
1744
- };
1745
- }
1746
- const { content } = parsedArgs;
1747
- try {
1748
- const result = await convexClient.mutation(
1749
- "mcp_memories:addMemory",
1750
- {
1751
- ...buildAuthArgs(config),
1752
- projectSlug: memoryTool.projectSlug,
1753
- content
1754
- }
1755
- );
1756
- let responseText;
1757
- if (result.linkedTicket) {
1758
- const ticketRef = `#${result.linkedTicket.number}`;
1759
- const fallbackNote = result.linkedViaFallback ? " (recently closed)" : "";
1760
- responseText = `\u2705 Memory recorded for ticket [${ticketRef}]${fallbackNote} in project "${memoryTool.projectSlug}".
1570
+ ## Usage
1571
+ Call the \`prompts_run\` tool with the prompt slug.
1761
1572
 
1762
- _Memory will be available in future context via [MEMORY_LOAD]._`;
1763
- } else {
1764
- responseText = `\u2705 Memory recorded in project "${memoryTool.projectSlug}".
1573
+ ## Available Prompts
1574
+ Use \`prompts_list\` to list all available prompts.
1765
1575
 
1766
- _Memory created without ticket linkage (no working ticket or recent closed ticket)._
1767
- _Memory will be available in future context via [MEMORY_LOAD]._`;
1768
- }
1769
- return {
1770
- content: [
1771
- {
1772
- type: "text",
1773
- text: responseText
1774
- }
1775
- ]
1776
- };
1777
- } catch (error) {
1778
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1779
- console.error(`[MCP] memories_add error:`, error);
1780
- return {
1781
- content: [
1782
- {
1783
- type: "text",
1784
- text: `Error adding memory: ${errorMessage}`
1785
- }
1786
- ],
1787
- isError: true
1788
- };
1789
- }
1790
- } else if (memoryTool.type === "list_all") {
1791
- const parsedArgs = parseMemoriesListAllArgs(request.params.arguments);
1792
- try {
1793
- const result = await convexClient.query(
1794
- "mcp_memories:listAllMemories",
1795
- {
1796
- ...buildAuthArgs(config),
1797
- projectSlug: memoryTool.projectSlug,
1798
- typeFilter: parsedArgs.typeFilter
1799
- }
1800
- );
1801
- const formatDate = (ts) => new Date(ts).toISOString().split("T")[0];
1802
- const userMemories = result.memories.filter((m) => m.type === "user");
1803
- const agentMemories = result.memories.filter((m) => m.type === "agent");
1804
- let output = `# Project Memories (${result.totalCount} total)
1576
+ ## Example
1577
+ /ppm:prompts_run code-review
1805
1578
 
1806
- `;
1807
- if (userMemories.length > 0) {
1808
- output += `## Rules (${userMemories.length})
1809
- `;
1810
- userMemories.forEach((m, i) => {
1811
- output += `${i + 1}. [${m.id}] ${m.content}
1812
- `;
1813
- });
1814
- output += "\n";
1815
- }
1816
- if (agentMemories.length > 0) {
1817
- output += `## Agent History (${agentMemories.length})
1818
- `;
1819
- agentMemories.forEach((m) => {
1820
- const ticketRef = m.ticketNumber ? ` [#${m.ticketNumber}]` : "";
1821
- output += `\u2022 [${m.id}]${ticketRef} ${m.content.substring(0, 100)}${m.content.length > 100 ? "..." : ""} _(${formatDate(m.createdAt)})_
1822
- `;
1823
- });
1579
+ This will execute the "code-review" prompt.`
1580
+ }
1824
1581
  }
1825
- return {
1826
- content: [{ type: "text", text: output }]
1827
- };
1828
- } catch (error) {
1829
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1830
- console.error(`[MCP] memories_list_all error:`, error);
1831
- return {
1832
- content: [{ type: "text", text: `Error listing memories: ${errorMessage}` }],
1833
- isError: true
1834
- };
1835
- }
1582
+ ]
1583
+ };
1584
+ }
1585
+ const userSlashCommand = slashCommandPrompts.find((p) => p.slug === promptName);
1586
+ if (userSlashCommand) {
1587
+ try {
1588
+ const result = await fetchAndExecuteAccountScopedPrompt(
1589
+ userSlashCommand.slug,
1590
+ config,
1591
+ convexClient
1592
+ );
1593
+ return {
1594
+ description: userSlashCommand.description || `Execute "${userSlashCommand.slug}"`,
1595
+ messages: result.messages.map((msg) => ({
1596
+ role: msg.role,
1597
+ content: { type: "text", text: msg.content.text }
1598
+ }))
1599
+ };
1600
+ } catch (error) {
1601
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1602
+ throw new Error(`Error executing slash command "${promptName}": ${errorMessage}`);
1836
1603
  }
1837
1604
  }
1605
+ throw new Error(`Unknown prompt: ${promptName}. Use prompts_run to execute prompts.`);
1606
+ });
1607
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
1608
+ return {
1609
+ tools: [
1610
+ // Ticket tools
1611
+ ...ticketTools.map((t) => ({
1612
+ name: t.name,
1613
+ description: t.description,
1614
+ inputSchema: t.inputSchema
1615
+ })),
1616
+ // Prompt tools (global)
1617
+ ...promptTools.map((t) => ({
1618
+ name: t.name,
1619
+ description: t.description,
1620
+ inputSchema: t.inputSchema
1621
+ })),
1622
+ // Memory tools
1623
+ ...memoryTools.map((t) => ({
1624
+ name: t.name,
1625
+ description: t.description,
1626
+ inputSchema: t.inputSchema
1627
+ })),
1628
+ // Per-prompt tools
1629
+ ...dynamicPromptTools.map((pt) => ({
1630
+ name: pt.name,
1631
+ description: pt.description,
1632
+ inputSchema: { type: "object", properties: {}, required: [] }
1633
+ }))
1634
+ ]
1635
+ };
1636
+ });
1637
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1638
+ const toolName = request.params.name;
1639
+ const args = request.params.arguments;
1640
+ if (toolName === "prompts_run") {
1641
+ return handlePromptsRun(args, config, convexClient);
1642
+ }
1643
+ if (toolName === "prompts_list") {
1644
+ return handlePromptsList(args, config, convexClient, accountScopedPrompts);
1645
+ }
1646
+ if (toolName === "prompts_update") {
1647
+ return handlePromptsUpdate(args, config, convexClient);
1648
+ }
1649
+ if (toolName === "tickets_work") {
1650
+ return handleTicketsWork(args, config, convexClient, projectSlug);
1651
+ }
1652
+ if (toolName === "tickets_create") {
1653
+ return handleTicketsCreate(args, config, convexClient, projectSlug);
1654
+ }
1655
+ if (toolName === "tickets_close") {
1656
+ return handleTicketsClose(args, config, convexClient, projectSlug);
1657
+ }
1658
+ if (toolName === "tickets_pause") {
1659
+ return handleTicketsPause(args, config, convexClient, projectSlug);
1660
+ }
1661
+ if (toolName === "tickets_search") {
1662
+ return handleTicketsSearch(args, config, convexClient, projectSlug);
1663
+ }
1664
+ if (toolName === "tickets_get") {
1665
+ return handleTicketsGet(args, config, convexClient, projectSlug);
1666
+ }
1667
+ if (toolName === "tickets_list") {
1668
+ return handleTicketsList(args, config, convexClient, projectSlug);
1669
+ }
1670
+ if (toolName === "tickets_update") {
1671
+ return handleTicketsUpdate(args, config, convexClient, projectSlug);
1672
+ }
1673
+ if (toolName === "memories_load") {
1674
+ return handleMemoriesLoad(args, config, convexClient, projectSlug);
1675
+ }
1676
+ if (toolName === "memories_load_extreme") {
1677
+ return handleMemoriesLoadExtreme(args, config, convexClient, projectSlug);
1678
+ }
1679
+ if (toolName === "memories_add") {
1680
+ return handleMemoriesAdd(args, config, convexClient, projectSlug);
1681
+ }
1682
+ if (toolName === "memories_list_all") {
1683
+ return handleMemoriesListAll(args, config, convexClient, projectSlug);
1684
+ }
1838
1685
  const promptTool = dynamicPromptTools.find((pt) => pt.name === toolName);
1839
1686
  if (promptTool) {
1840
1687
  try {
@@ -1844,9 +1691,7 @@ _Memory will be available in future context via [MEMORY_LOAD]._`;
1844
1691
  convexClient
1845
1692
  );
1846
1693
  const promptText = result.messages.map((msg) => msg.content.text).join("\n\n");
1847
- return {
1848
- content: [{ type: "text", text: promptText }]
1849
- };
1694
+ return { content: [{ type: "text", text: promptText }] };
1850
1695
  } catch (error) {
1851
1696
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
1852
1697
  console.error(`[MCP] ${toolName} error:`, error);
@@ -1856,7 +1701,15 @@ _Memory will be available in future context via [MEMORY_LOAD]._`;
1856
1701
  };
1857
1702
  }
1858
1703
  }
1859
- throw new Error(`Unknown tool: ${toolName}. Use prompts_run to execute prompts by name, or check available tools.`);
1704
+ return {
1705
+ content: [
1706
+ {
1707
+ type: "text",
1708
+ text: `Unknown tool: ${toolName}. Use prompts_run to execute prompts by name, or check available tools.`
1709
+ }
1710
+ ],
1711
+ isError: true
1712
+ };
1860
1713
  });
1861
1714
  const transport = new StdioServerTransport();
1862
1715
  await server.connect(transport);
@@ -1872,28 +1725,63 @@ _Memory will be available in future context via [MEMORY_LOAD]._`;
1872
1725
  return new Promise(() => {
1873
1726
  });
1874
1727
  }
1875
- async function validateProjectToken(client, token) {
1876
- try {
1877
- const typedClient = client;
1878
- const result = await typedClient.query(
1879
- "projects:validateProjectToken",
1880
- { token }
1881
- );
1882
- if (result && result.valid) {
1883
- return {
1884
- valid: true,
1885
- projectId: result.projectId,
1886
- projectSlug: result.projectSlug,
1887
- ownerId: result.ownerId
1888
- };
1889
- }
1890
- return { valid: false, error: "Invalid project token" };
1891
- } catch (error) {
1892
- return {
1893
- valid: false,
1894
- error: error instanceof Error ? error.message : "Unknown error"
1895
- };
1728
+ function buildTicketPromptResponse(type, toolName, projectSlug) {
1729
+ let promptContent;
1730
+ let description;
1731
+ if (type === "work") {
1732
+ description = `Get work from the "${projectSlug}" project`;
1733
+ promptContent = `Get work from the "${projectSlug}" project.
1734
+
1735
+ Call the \`tickets_work\` tool to get the next ticket from the open queue.
1736
+
1737
+ You can also specify a ticket: /ppm:tickets_work <number-or-slug>
1738
+
1739
+ This unified command handles both opening new tickets and resuming in-progress work.`;
1740
+ } else if (type === "create") {
1741
+ description = `Create a new ticket in "${projectSlug}"`;
1742
+ promptContent = `Create a new ticket in the "${projectSlug}" project queue.
1743
+
1744
+ ## How This Works
1745
+ The user provides **instructions** (not raw content). You interpret those instructions to generate the ticket.
1746
+
1747
+ ## Instructions
1748
+ 1. **Read the user's request** - they may reference a file, ask you to summarize a session, or describe what they want
1749
+ 2. **Process the input** - read files, extract relevant sections, summarize as needed
1750
+ 3. **Generate ticket content** with a clear, descriptive title as the first line
1751
+ 4. **Call the tool** with the generated content
1752
+
1753
+ ## Content Format
1754
+ \`\`\`
1755
+ [Clear descriptive title - this becomes the slug]
1756
+
1757
+ [Body content - tasks, description, context, etc.]
1758
+ \`\`\`
1759
+
1760
+ ## Examples
1761
+ - "create a ticket from /path/to/plan.md" \u2192 Read file, use as ticket content
1762
+ - "summarize our brainstorm into a ticket" \u2192 Extract key points from conversation
1763
+ - "create a ticket for the auth bug we discussed" \u2192 Generate from session context
1764
+ - "just the tasks from this file" \u2192 Extract only task items
1765
+
1766
+ Call the \`${toolName}\` tool with the final generated content.`;
1767
+ } else if (type === "close") {
1768
+ description = `Close a working ticket in "${projectSlug}"`;
1769
+ promptContent = `Close a working ticket in the "${projectSlug}" project.
1770
+
1771
+ Call the \`${toolName}\` tool with the ticket number or slug.`;
1772
+ } else {
1773
+ description = `Use the ${toolName} tool`;
1774
+ promptContent = `Use the \`${toolName}\` tool.`;
1896
1775
  }
1776
+ return {
1777
+ description,
1778
+ messages: [
1779
+ {
1780
+ role: "user",
1781
+ content: { type: "text", text: promptContent }
1782
+ }
1783
+ ]
1784
+ };
1897
1785
  }
1898
1786
 
1899
1787
  // src/index.ts