@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 +1455 -1595
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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/
|
|
209
|
-
function
|
|
210
|
-
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
|
|
466
|
-
|
|
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
|
-
##
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
##
|
|
475
|
-
|
|
476
|
-
[Clear descriptive title - this becomes the slug]
|
|
680
|
+
## Description
|
|
681
|
+
${readResult.description || "No description"}
|
|
477
682
|
|
|
478
|
-
|
|
683
|
+
## Content
|
|
479
684
|
\`\`\`
|
|
685
|
+
${readResult.content}
|
|
686
|
+
\`\`\`${contextSection}
|
|
480
687
|
|
|
481
|
-
|
|
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
|
-
|
|
493
|
-
|
|
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 (
|
|
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
|
-
|
|
513
|
-
messages: [
|
|
704
|
+
content: [
|
|
514
705
|
{
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
type: "text",
|
|
518
|
-
text: `List all available prompts.
|
|
706
|
+
type: "text",
|
|
707
|
+
text: `\u2705 ${writeResult.message}
|
|
519
708
|
|
|
520
|
-
|
|
521
|
-
}
|
|
709
|
+
${resourceLabel}: ${writeResult.slug}${versionInfo}${updatedInfo}`
|
|
522
710
|
}
|
|
523
711
|
]
|
|
524
712
|
};
|
|
525
713
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
##
|
|
541
|
-
|
|
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
|
-
|
|
544
|
-
Use \`prompts_list\` to list all available prompts.
|
|
775
|
+
${workResult.content}
|
|
545
776
|
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
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
|
-
|
|
553
|
-
messages: [
|
|
901
|
+
content: [
|
|
554
902
|
{
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
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
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
${
|
|
944
|
+
Paused at: ${new Date(result.pausedAt).toISOString()}
|
|
899
945
|
|
|
900
|
-
|
|
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
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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:
|
|
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
|
-
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1038
|
+
);
|
|
1039
|
+
if (!result) {
|
|
1040
|
+
return {
|
|
1041
|
+
content: [
|
|
968
1042
|
{
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
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
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
-
${
|
|
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
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1611
|
-
|
|
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
|
|
1638
|
-
if (
|
|
1639
|
-
if (
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
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
|
-
|
|
1646
|
-
|
|
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:
|
|
1553
|
+
text: `Execute the "${promptSlug}" prompt.
|
|
1678
1554
|
|
|
1679
|
-
${
|
|
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
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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
|
-
|
|
1791
|
-
|
|
1792
|
-
responseText = `\u2705 Memory recorded in project "${memoryTool.projectSlug}".
|
|
1570
|
+
## Usage
|
|
1571
|
+
Call the \`prompts_run\` tool with the prompt slug.
|
|
1793
1572
|
|
|
1794
|
-
|
|
1795
|
-
|
|
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
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
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
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
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
|