@promptprojectmanager/mcp-server 4.9.5 → 4.9.7

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