@verygoodplugins/mcp-freescout 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +26 -0
- package/.github/workflows/ci.yml +34 -0
- package/.github/workflows/release-please.yml +102 -0
- package/.github/workflows/security.yml +53 -0
- package/.prettierignore +24 -0
- package/.prettierrc +10 -0
- package/AGENTS.md +6 -0
- package/CHANGELOG.md +147 -0
- package/CLAUDE.md +111 -0
- package/README.md +183 -167
- package/TESTING.md +98 -0
- package/dist/freescout-api.d.ts +32 -4
- package/dist/freescout-api.d.ts.map +1 -1
- package/dist/freescout-api.js +195 -45
- package/dist/freescout-api.js.map +1 -1
- package/dist/index.js +237 -778
- package/dist/index.js.map +1 -1
- package/dist/ticket-analyzer.d.ts.map +1 -1
- package/dist/ticket-analyzer.js +27 -23
- package/dist/ticket-analyzer.js.map +1 -1
- package/dist/types.d.ts +221 -72
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +88 -1
- package/dist/types.js.map +1 -1
- package/eslint.config.mjs +24 -0
- package/jest.setup.cjs +48 -0
- package/package.json +19 -13
- package/server.json +87 -0
package/dist/index.js
CHANGED
|
@@ -1,71 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
4
|
import { config } from 'dotenv';
|
|
6
|
-
import {
|
|
5
|
+
import { z } from 'zod';
|
|
7
6
|
import { FreeScoutAPI } from './freescout-api.js';
|
|
8
7
|
import { TicketAnalyzer } from './ticket-analyzer.js';
|
|
8
|
+
import { ConversationSchema, TicketAnalysisSchema, SearchFiltersSchema } from './types.js';
|
|
9
9
|
// Load environment variables
|
|
10
10
|
config();
|
|
11
11
|
// Validate required environment variables
|
|
12
12
|
const FREESCOUT_URL = process.env.FREESCOUT_URL;
|
|
13
13
|
const FREESCOUT_API_KEY = process.env.FREESCOUT_API_KEY;
|
|
14
14
|
const DEFAULT_USER_ID = parseInt(process.env.FREESCOUT_DEFAULT_USER_ID || '1');
|
|
15
|
-
// WORKING_DIRECTORY defaults to current working directory if not specified
|
|
16
|
-
// This allows the server to work with the current project context automatically
|
|
17
|
-
const WORKING_DIRECTORY = process.env.WORKING_DIRECTORY || process.cwd();
|
|
18
|
-
// Helper function to check if GitHub CLI is available
|
|
19
|
-
function isGhAvailable() {
|
|
20
|
-
try {
|
|
21
|
-
execSync('gh --version', {
|
|
22
|
-
stdio: 'pipe',
|
|
23
|
-
timeout: 5000
|
|
24
|
-
});
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
// Helper function to get GitHub repo using gh CLI
|
|
32
|
-
function getGitHubRepo() {
|
|
33
|
-
if (process.env.GITHUB_REPO) {
|
|
34
|
-
return process.env.GITHUB_REPO;
|
|
35
|
-
}
|
|
36
|
-
if (!isGhAvailable()) {
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
try {
|
|
40
|
-
// Use gh to get repo info - this is more reliable than parsing git remotes
|
|
41
|
-
const repoInfo = execSync('gh repo view --json nameWithOwner', {
|
|
42
|
-
cwd: WORKING_DIRECTORY,
|
|
43
|
-
encoding: 'utf-8',
|
|
44
|
-
stdio: 'pipe'
|
|
45
|
-
}).trim();
|
|
46
|
-
const parsed = JSON.parse(repoInfo);
|
|
47
|
-
return parsed.nameWithOwner;
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
// gh command failed - might not be in a GitHub repo or not authenticated
|
|
51
|
-
return undefined;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// Helper function to check if Git operations are available
|
|
55
|
-
function isGitAvailable() {
|
|
56
|
-
try {
|
|
57
|
-
execSync('git --version', {
|
|
58
|
-
stdio: 'pipe',
|
|
59
|
-
timeout: 5000
|
|
60
|
-
});
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
catch (error) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
// Get GitHub configuration - no token needed, gh CLI handles auth
|
|
68
|
-
const GITHUB_REPO = getGitHubRepo();
|
|
69
15
|
if (!FREESCOUT_URL || !FREESCOUT_API_KEY) {
|
|
70
16
|
console.error('Missing required environment variables: FREESCOUT_URL and FREESCOUT_API_KEY');
|
|
71
17
|
process.exit(1);
|
|
@@ -73,751 +19,264 @@ if (!FREESCOUT_URL || !FREESCOUT_API_KEY) {
|
|
|
73
19
|
// Initialize API and analyzer
|
|
74
20
|
const api = new FreeScoutAPI(FREESCOUT_URL, FREESCOUT_API_KEY);
|
|
75
21
|
const analyzer = new TicketAnalyzer();
|
|
76
|
-
// Create MCP server
|
|
77
|
-
const server = new
|
|
22
|
+
// Create MCP server with new McpServer class
|
|
23
|
+
const server = new McpServer({
|
|
78
24
|
name: 'mcp-freescout',
|
|
79
|
-
version: '
|
|
80
|
-
}, {
|
|
81
|
-
capabilities: {
|
|
82
|
-
tools: {},
|
|
83
|
-
},
|
|
25
|
+
version: '2.0.0',
|
|
84
26
|
});
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
},
|
|
97
|
-
includeThreads: {
|
|
98
|
-
type: 'boolean',
|
|
99
|
-
description: 'Include all conversation threads (default: true)',
|
|
100
|
-
default: true,
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
required: ['ticket'],
|
|
104
|
-
},
|
|
27
|
+
// Tool 1: Get Ticket
|
|
28
|
+
server.registerTool('freescout_get_ticket', {
|
|
29
|
+
title: 'Get FreeScout Ticket',
|
|
30
|
+
description: 'Fetch and analyze a FreeScout ticket by ID or URL',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
ticket: z.string().describe('Ticket ID, ticket number, or FreeScout URL'),
|
|
33
|
+
includeThreads: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.optional()
|
|
36
|
+
.default(true)
|
|
37
|
+
.describe('Include all conversation threads'),
|
|
105
38
|
},
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
39
|
+
outputSchema: ConversationSchema,
|
|
40
|
+
}, async ({ ticket, includeThreads }) => {
|
|
41
|
+
const ticketId = api.parseTicketInput(ticket);
|
|
42
|
+
const conversation = await api.getConversation(ticketId, includeThreads ?? true);
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text', text: JSON.stringify(conversation, null, 2) }],
|
|
45
|
+
structuredContent: conversation,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
// Tool 2: Analyze Ticket
|
|
49
|
+
server.registerTool('freescout_analyze_ticket', {
|
|
50
|
+
title: 'Analyze FreeScout Ticket',
|
|
51
|
+
description: 'Analyze a FreeScout ticket to determine issue type, root cause, and suggested solution',
|
|
52
|
+
inputSchema: {
|
|
53
|
+
ticket: z.string().describe('Ticket ID, ticket number, or FreeScout URL'),
|
|
119
54
|
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
},
|
|
139
|
-
required: ['ticket', 'note'],
|
|
140
|
-
},
|
|
55
|
+
outputSchema: TicketAnalysisSchema,
|
|
56
|
+
}, async ({ ticket }) => {
|
|
57
|
+
const ticketId = api.parseTicketInput(ticket);
|
|
58
|
+
const conversation = await api.getConversation(ticketId, true);
|
|
59
|
+
const analysis = analyzer.analyzeConversation(conversation);
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: 'text', text: JSON.stringify(analysis, null, 2) }],
|
|
62
|
+
structuredContent: analysis,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
// Tool 3: Add Note
|
|
66
|
+
server.registerTool('freescout_add_note', {
|
|
67
|
+
title: 'Add Note to Ticket',
|
|
68
|
+
description: 'Add an internal note to a FreeScout ticket',
|
|
69
|
+
inputSchema: {
|
|
70
|
+
ticket: z.string().describe('Ticket ID, ticket number, or FreeScout URL'),
|
|
71
|
+
note: z.string().describe('The note content to add'),
|
|
72
|
+
userId: z.number().optional().describe('User ID for the note (default: from env)'),
|
|
141
73
|
},
|
|
142
|
-
{
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
type: 'object',
|
|
147
|
-
properties: {
|
|
148
|
-
ticket: {
|
|
149
|
-
type: 'string',
|
|
150
|
-
description: 'Ticket ID, ticket number, or FreeScout URL',
|
|
151
|
-
},
|
|
152
|
-
status: {
|
|
153
|
-
type: 'string',
|
|
154
|
-
enum: ['active', 'pending', 'closed', 'spam'],
|
|
155
|
-
description: 'New ticket status',
|
|
156
|
-
},
|
|
157
|
-
assignTo: {
|
|
158
|
-
type: 'number',
|
|
159
|
-
description: 'User ID to assign the ticket to',
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
required: ['ticket'],
|
|
163
|
-
},
|
|
74
|
+
outputSchema: {
|
|
75
|
+
success: z.boolean(),
|
|
76
|
+
message: z.string(),
|
|
77
|
+
ticketId: z.string(),
|
|
164
78
|
},
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
79
|
+
}, async ({ ticket, note, userId }) => {
|
|
80
|
+
const ticketId = api.parseTicketInput(ticket);
|
|
81
|
+
const actualUserId = userId ?? DEFAULT_USER_ID;
|
|
82
|
+
await api.addThread(ticketId, 'note', note, actualUserId);
|
|
83
|
+
const output = {
|
|
84
|
+
success: true,
|
|
85
|
+
message: `Note added to ticket #${ticketId}`,
|
|
86
|
+
ticketId,
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
content: [{ type: 'text', text: output.message }],
|
|
90
|
+
structuredContent: output,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
// Tool 4: Update Ticket
|
|
94
|
+
server.registerTool('freescout_update_ticket', {
|
|
95
|
+
title: 'Update Ticket Status/Assignment',
|
|
96
|
+
description: 'Update ticket status and/or assignment',
|
|
97
|
+
inputSchema: {
|
|
98
|
+
ticket: z.string().describe('Ticket ID, ticket number, or FreeScout URL'),
|
|
99
|
+
status: z
|
|
100
|
+
.enum(['active', 'pending', 'closed', 'spam'])
|
|
101
|
+
.optional()
|
|
102
|
+
.describe('New ticket status'),
|
|
103
|
+
assignTo: z.number().optional().describe('User ID to assign the ticket to'),
|
|
186
104
|
},
|
|
187
|
-
{
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
type: 'object',
|
|
192
|
-
properties: {
|
|
193
|
-
ticket: {
|
|
194
|
-
type: 'string',
|
|
195
|
-
description: 'Ticket ID, ticket number, or FreeScout URL',
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
required: ['ticket'],
|
|
199
|
-
},
|
|
105
|
+
outputSchema: {
|
|
106
|
+
success: z.boolean(),
|
|
107
|
+
message: z.string(),
|
|
108
|
+
ticketId: z.string(),
|
|
200
109
|
},
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
110
|
+
}, async ({ ticket, status, assignTo }) => {
|
|
111
|
+
const ticketId = api.parseTicketInput(ticket);
|
|
112
|
+
const updates = { byUser: DEFAULT_USER_ID };
|
|
113
|
+
if (status)
|
|
114
|
+
updates.status = status;
|
|
115
|
+
if (assignTo)
|
|
116
|
+
updates.assignTo = assignTo;
|
|
117
|
+
await api.updateConversation(ticketId, updates);
|
|
118
|
+
const output = {
|
|
119
|
+
success: true,
|
|
120
|
+
message: `Ticket #${ticketId} updated successfully`,
|
|
121
|
+
ticketId,
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
content: [{ type: 'text', text: output.message }],
|
|
125
|
+
structuredContent: output,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
// Tool 5: Create Draft Reply
|
|
129
|
+
server.registerTool('freescout_create_draft_reply', {
|
|
130
|
+
title: 'Create Draft Reply',
|
|
131
|
+
description: 'Create a draft reply in FreeScout that can be edited before sending',
|
|
132
|
+
inputSchema: {
|
|
133
|
+
ticket: z.string().describe('Ticket ID, ticket number, or FreeScout URL'),
|
|
134
|
+
replyText: z.string().describe('The draft reply content (generated by the LLM)'),
|
|
135
|
+
userId: z
|
|
136
|
+
.number()
|
|
137
|
+
.optional()
|
|
138
|
+
.describe('User ID creating the draft (defaults to env setting)'),
|
|
228
139
|
},
|
|
229
|
-
{
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
properties: {},
|
|
235
|
-
required: [],
|
|
236
|
-
},
|
|
140
|
+
outputSchema: {
|
|
141
|
+
success: z.boolean(),
|
|
142
|
+
message: z.string(),
|
|
143
|
+
ticketId: z.string(),
|
|
144
|
+
draftId: z.number(),
|
|
237
145
|
},
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
type: 'string',
|
|
254
|
-
description: 'Base branch to create from (default: master)',
|
|
255
|
-
default: 'master',
|
|
256
|
-
},
|
|
146
|
+
}, async ({ ticket, replyText, userId }) => {
|
|
147
|
+
const ticketId = api.parseTicketInput(ticket);
|
|
148
|
+
const actualUserId = userId ?? DEFAULT_USER_ID;
|
|
149
|
+
const draftThread = await api.createDraftReply(ticketId, replyText, actualUserId);
|
|
150
|
+
const output = {
|
|
151
|
+
success: true,
|
|
152
|
+
message: `Draft reply created successfully in FreeScout ticket #${ticketId}`,
|
|
153
|
+
ticketId,
|
|
154
|
+
draftId: draftThread.id,
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
content: [
|
|
158
|
+
{
|
|
159
|
+
type: 'text',
|
|
160
|
+
text: `✅ ${output.message}\n\nDraft ID: ${draftThread.id}\n\nThe draft reply is now saved in FreeScout and can be reviewed, edited, and sent from the FreeScout interface.`,
|
|
257
161
|
},
|
|
258
|
-
|
|
259
|
-
|
|
162
|
+
],
|
|
163
|
+
structuredContent: output,
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
// Tool 6: Get Ticket Context
|
|
167
|
+
server.registerTool('freescout_get_ticket_context', {
|
|
168
|
+
title: 'Get Ticket Context',
|
|
169
|
+
description: 'Get ticket context and customer info to help draft personalized replies',
|
|
170
|
+
inputSchema: {
|
|
171
|
+
ticket: z.string().describe('Ticket ID, ticket number, or FreeScout URL'),
|
|
260
172
|
},
|
|
261
|
-
{
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
},
|
|
173
|
+
outputSchema: {
|
|
174
|
+
ticketId: z.string(),
|
|
175
|
+
customer: z.object({
|
|
176
|
+
name: z.string(),
|
|
177
|
+
email: z.string(),
|
|
178
|
+
}),
|
|
179
|
+
subject: z.string(),
|
|
180
|
+
status: z.string(),
|
|
181
|
+
issueDescription: z.string(),
|
|
182
|
+
customerMessages: z.array(z.object({
|
|
183
|
+
date: z.string(),
|
|
184
|
+
content: z.string(),
|
|
185
|
+
})),
|
|
186
|
+
teamMessages: z.array(z.object({
|
|
187
|
+
date: z.string(),
|
|
188
|
+
content: z.string(),
|
|
189
|
+
})),
|
|
190
|
+
analysis: z.object({
|
|
191
|
+
isBug: z.boolean(),
|
|
192
|
+
isThirdPartyIssue: z.boolean(),
|
|
193
|
+
testedByTeam: z.boolean(),
|
|
194
|
+
rootCause: z.string().optional(),
|
|
195
|
+
}),
|
|
274
196
|
},
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
description: 'PR description/body',
|
|
288
|
-
},
|
|
289
|
-
ticketId: {
|
|
290
|
-
type: 'string',
|
|
291
|
-
description: 'FreeScout ticket ID for reference',
|
|
292
|
-
},
|
|
293
|
-
branch: {
|
|
294
|
-
type: 'string',
|
|
295
|
-
description: 'Branch name (defaults to current branch)',
|
|
296
|
-
},
|
|
297
|
-
baseBranch: {
|
|
298
|
-
type: 'string',
|
|
299
|
-
description: 'Base branch (default: master)',
|
|
300
|
-
default: 'master',
|
|
301
|
-
},
|
|
302
|
-
draft: {
|
|
303
|
-
type: 'boolean',
|
|
304
|
-
description: 'Create as draft PR (default: false)',
|
|
305
|
-
default: false,
|
|
306
|
-
},
|
|
307
|
-
},
|
|
308
|
-
required: ['title', 'body'],
|
|
197
|
+
}, async ({ ticket }) => {
|
|
198
|
+
const ticketId = api.parseTicketInput(ticket);
|
|
199
|
+
const conversation = await api.getConversation(ticketId, true);
|
|
200
|
+
const analysis = analyzer.analyzeConversation(conversation);
|
|
201
|
+
const threads = conversation._embedded?.threads || [];
|
|
202
|
+
const customerMessages = threads.filter((t) => t.type === 'customer');
|
|
203
|
+
const teamMessages = threads.filter((t) => t.type === 'message' || t.type === 'note');
|
|
204
|
+
const context = {
|
|
205
|
+
ticketId,
|
|
206
|
+
customer: {
|
|
207
|
+
name: analysis.customerName,
|
|
208
|
+
email: analysis.customerEmail,
|
|
309
209
|
},
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
default: true,
|
|
329
|
-
},
|
|
330
|
-
},
|
|
331
|
-
required: ['ticket'],
|
|
210
|
+
subject: conversation.subject,
|
|
211
|
+
status: conversation.status,
|
|
212
|
+
issueDescription: analysis.issueDescription,
|
|
213
|
+
customerMessages: customerMessages.map((m) => ({
|
|
214
|
+
date: m.created_at,
|
|
215
|
+
content: analyzer.stripHtml(m.body).substring(0, 500) +
|
|
216
|
+
(analyzer.stripHtml(m.body).length > 500 ? '...' : ''),
|
|
217
|
+
})),
|
|
218
|
+
teamMessages: teamMessages.slice(-3).map((m) => ({
|
|
219
|
+
date: m.created_at,
|
|
220
|
+
content: analyzer.stripHtml(m.body).substring(0, 300) +
|
|
221
|
+
(analyzer.stripHtml(m.body).length > 300 ? '...' : ''),
|
|
222
|
+
})),
|
|
223
|
+
analysis: {
|
|
224
|
+
isBug: analysis.isBug,
|
|
225
|
+
isThirdPartyIssue: analysis.isThirdPartyIssue,
|
|
226
|
+
testedByTeam: analysis.testedByTeam,
|
|
227
|
+
rootCause: analysis.rootCause,
|
|
332
228
|
},
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: 'text', text: JSON.stringify(context, null, 2) }],
|
|
232
|
+
structuredContent: context,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
// Tool 7: Search Tickets
|
|
236
|
+
server.registerTool('freescout_search_tickets', {
|
|
237
|
+
title: 'Search FreeScout Tickets',
|
|
238
|
+
description: 'Search for FreeScout tickets with explicit filter parameters. Use assignee: "unassigned" for unassigned tickets, or assignee: number for specific user. Supports relative time filters like "7d", "24h".',
|
|
239
|
+
inputSchema: SearchFiltersSchema,
|
|
240
|
+
outputSchema: {
|
|
241
|
+
conversations: z.array(ConversationSchema),
|
|
242
|
+
totalCount: z.number(),
|
|
243
|
+
page: z.number().optional(),
|
|
244
|
+
totalPages: z.number().optional(),
|
|
333
245
|
},
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
246
|
+
}, async (filters) => {
|
|
247
|
+
const results = await api.searchConversations(filters);
|
|
248
|
+
const output = {
|
|
249
|
+
conversations: results._embedded?.conversations || [],
|
|
250
|
+
totalCount: results.page?.total_elements || 0,
|
|
251
|
+
page: results.page?.number,
|
|
252
|
+
totalPages: results.page?.total_pages,
|
|
253
|
+
};
|
|
337
254
|
return {
|
|
338
|
-
|
|
255
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
256
|
+
structuredContent: output,
|
|
339
257
|
};
|
|
340
258
|
});
|
|
341
|
-
//
|
|
342
|
-
server.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
text: JSON.stringify(conversation, null, 2),
|
|
357
|
-
},
|
|
358
|
-
],
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
case 'freescout_analyze_ticket': {
|
|
362
|
-
const ticketId = api.parseTicketInput(args.ticket);
|
|
363
|
-
const conversation = await api.getConversation(ticketId, true);
|
|
364
|
-
const analysis = analyzer.analyzeConversation(conversation);
|
|
365
|
-
return {
|
|
366
|
-
content: [
|
|
367
|
-
{
|
|
368
|
-
type: 'text',
|
|
369
|
-
text: JSON.stringify(analysis, null, 2),
|
|
370
|
-
},
|
|
371
|
-
],
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
case 'freescout_add_note': {
|
|
375
|
-
const ticketId = api.parseTicketInput(args.ticket);
|
|
376
|
-
const userId = args.userId || DEFAULT_USER_ID;
|
|
377
|
-
const thread = await api.addThread(ticketId, 'note', args.note, userId);
|
|
378
|
-
return {
|
|
379
|
-
content: [
|
|
380
|
-
{
|
|
381
|
-
type: 'text',
|
|
382
|
-
text: `Note added to ticket #${ticketId}`,
|
|
383
|
-
},
|
|
384
|
-
],
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
case 'freescout_update_ticket': {
|
|
388
|
-
const ticketId = api.parseTicketInput(args.ticket);
|
|
389
|
-
const updates = {
|
|
390
|
-
byUser: DEFAULT_USER_ID,
|
|
391
|
-
};
|
|
392
|
-
if (args.status) {
|
|
393
|
-
updates.status = args.status;
|
|
394
|
-
}
|
|
395
|
-
if (args.assignTo) {
|
|
396
|
-
updates.assignTo = args.assignTo;
|
|
397
|
-
}
|
|
398
|
-
const updated = await api.updateConversation(ticketId, updates);
|
|
399
|
-
return {
|
|
400
|
-
content: [
|
|
401
|
-
{
|
|
402
|
-
type: 'text',
|
|
403
|
-
text: `Ticket #${ticketId} updated successfully`,
|
|
404
|
-
},
|
|
405
|
-
],
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
case 'freescout_create_draft_reply': {
|
|
409
|
-
const ticketId = api.parseTicketInput(args.ticket);
|
|
410
|
-
const replyText = args.replyText;
|
|
411
|
-
const userId = args.userId || DEFAULT_USER_ID;
|
|
412
|
-
try {
|
|
413
|
-
const draftThread = await api.createDraftReply(ticketId, replyText, userId);
|
|
414
|
-
return {
|
|
415
|
-
content: [
|
|
416
|
-
{
|
|
417
|
-
type: 'text',
|
|
418
|
-
text: `✅ Draft reply created successfully in FreeScout ticket #${ticketId}\n\nDraft ID: ${draftThread.id}\n\nThe draft reply is now saved in FreeScout and can be reviewed, edited, and sent from the FreeScout interface.`,
|
|
419
|
-
},
|
|
420
|
-
],
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
catch (error) {
|
|
424
|
-
throw new Error(`Failed to create draft reply: ${error.message}`);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
case 'freescout_get_ticket_context': {
|
|
428
|
-
const ticketId = api.parseTicketInput(args.ticket);
|
|
429
|
-
const conversation = await api.getConversation(ticketId, true);
|
|
430
|
-
const analysis = analyzer.analyzeConversation(conversation);
|
|
431
|
-
const threads = conversation._embedded?.threads || [];
|
|
432
|
-
const customerMessages = threads.filter(t => t.type === 'customer');
|
|
433
|
-
const teamMessages = threads.filter(t => t.type === 'message' || t.type === 'note');
|
|
434
|
-
const context = {
|
|
435
|
-
ticketId,
|
|
436
|
-
customer: {
|
|
437
|
-
name: analysis.customerName,
|
|
438
|
-
email: analysis.customerEmail,
|
|
439
|
-
},
|
|
440
|
-
subject: conversation.subject,
|
|
441
|
-
status: conversation.status,
|
|
442
|
-
issueDescription: analysis.issueDescription,
|
|
443
|
-
customerMessages: customerMessages.map(m => ({
|
|
444
|
-
date: m.created_at,
|
|
445
|
-
content: analyzer.stripHtml(m.body).substring(0, 500) + (analyzer.stripHtml(m.body).length > 500 ? '...' : ''),
|
|
446
|
-
})),
|
|
447
|
-
teamMessages: teamMessages.slice(-3).map(m => ({
|
|
448
|
-
date: m.created_at,
|
|
449
|
-
content: analyzer.stripHtml(m.body).substring(0, 300) + (analyzer.stripHtml(m.body).length > 300 ? '...' : ''),
|
|
450
|
-
})),
|
|
451
|
-
analysis: {
|
|
452
|
-
isBug: analysis.isBug,
|
|
453
|
-
isThirdPartyIssue: analysis.isThirdPartyIssue,
|
|
454
|
-
testedByTeam: analysis.testedByTeam,
|
|
455
|
-
rootCause: analysis.rootCause,
|
|
456
|
-
},
|
|
457
|
-
};
|
|
458
|
-
return {
|
|
459
|
-
content: [
|
|
460
|
-
{
|
|
461
|
-
type: 'text',
|
|
462
|
-
text: JSON.stringify(context, null, 2),
|
|
463
|
-
},
|
|
464
|
-
],
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
case 'freescout_search_tickets': {
|
|
468
|
-
// Always default to 'published' state unless user explicitly requests 'deleted'
|
|
469
|
-
const state = args.state || 'published';
|
|
470
|
-
const query = args.query;
|
|
471
|
-
let results;
|
|
472
|
-
// If searching for unassigned tickets, use the list endpoint which properly supports filtering
|
|
473
|
-
if (query.includes('assignee:null') || query.includes('unassigned')) {
|
|
474
|
-
results = await api.listConversations(args.status, state, null // null assignee for unassigned tickets
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
else {
|
|
478
|
-
// Use search for other queries
|
|
479
|
-
results = await api.searchConversations(query, args.status, state, args.mailboxId);
|
|
480
|
-
/**
|
|
481
|
-
* LIMITATION: The FreeScout searchConversations API endpoint does not respect
|
|
482
|
-
* the 'state' parameter (published/draft). Unlike the list endpoint, which
|
|
483
|
-
* correctly filters by state, the search endpoint ignores this parameter and
|
|
484
|
-
* returns all conversations regardless of state.
|
|
485
|
-
*
|
|
486
|
-
* WORKAROUND: We apply client-side filtering below when state='published'.
|
|
487
|
-
*
|
|
488
|
-
* PERFORMANCE IMPLICATIONS:
|
|
489
|
-
* - Client-side filtering may be expensive for large result sets
|
|
490
|
-
* - Pagination counts may be inaccurate (total_elements reflects filtered count)
|
|
491
|
-
* - Results may be incomplete if the API returns paginated data
|
|
492
|
-
*
|
|
493
|
-
* ACTION ITEM: Verify with FreeScout maintainers whether the searchConversations
|
|
494
|
-
* endpoint should accept the 'state' parameter or if API documentation only
|
|
495
|
-
* applies to the list endpoint. Consider filing an issue or feature request.
|
|
496
|
-
*/
|
|
497
|
-
if (state === 'published' && results._embedded?.conversations) {
|
|
498
|
-
const originalCount = results._embedded.conversations.length;
|
|
499
|
-
results._embedded.conversations = results._embedded.conversations.filter((conversation) => conversation.state === 'published');
|
|
500
|
-
const filteredCount = results._embedded.conversations.length;
|
|
501
|
-
console.error(`[WARNING] searchConversations endpoint does not respect 'state' parameter. ` +
|
|
502
|
-
`Applied client-side filtering for state='${state}'. ` +
|
|
503
|
-
`Original count: ${originalCount}, Filtered count: ${filteredCount}. ` +
|
|
504
|
-
`This may be expensive for large result sets and incomplete with pagination.`);
|
|
505
|
-
// Update the total count to reflect filtered results
|
|
506
|
-
if (results.page) {
|
|
507
|
-
results.page.total_elements = results._embedded.conversations.length;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
return {
|
|
512
|
-
content: [
|
|
513
|
-
{
|
|
514
|
-
type: 'text',
|
|
515
|
-
text: JSON.stringify(results, null, 2),
|
|
516
|
-
},
|
|
517
|
-
],
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
case 'freescout_get_mailboxes': {
|
|
521
|
-
const mailboxes = await api.getMailboxes();
|
|
522
|
-
return {
|
|
523
|
-
content: [
|
|
524
|
-
{
|
|
525
|
-
type: 'text',
|
|
526
|
-
text: JSON.stringify(mailboxes, null, 2),
|
|
527
|
-
},
|
|
528
|
-
],
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
case 'git_create_worktree': {
|
|
532
|
-
if (!isGitAvailable()) {
|
|
533
|
-
return {
|
|
534
|
-
content: [
|
|
535
|
-
{
|
|
536
|
-
type: 'text',
|
|
537
|
-
text: '⚠️ Git is not available in this environment. Please create the worktree manually:\n\n' +
|
|
538
|
-
`git worktree add worktrees/ticket-${args.ticketId} -b ${args.branchName || `fix/freescout-${args.ticketId}`} ${args.baseBranch || 'master'}`,
|
|
539
|
-
},
|
|
540
|
-
],
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
const ticketId = args.ticketId;
|
|
544
|
-
const branchName = args.branchName || `fix/freescout-${ticketId}`;
|
|
545
|
-
const baseBranch = args.baseBranch || 'master';
|
|
546
|
-
const worktreeDir = `${WORKING_DIRECTORY}/worktrees/ticket-${ticketId}`;
|
|
547
|
-
try {
|
|
548
|
-
// Create worktrees directory if it doesn't exist
|
|
549
|
-
execSync(`mkdir -p "${WORKING_DIRECTORY}/worktrees"`, {
|
|
550
|
-
cwd: WORKING_DIRECTORY,
|
|
551
|
-
stdio: 'pipe'
|
|
552
|
-
});
|
|
553
|
-
// Create worktree
|
|
554
|
-
execSync(`git worktree add "${worktreeDir}" -b "${branchName}" ${baseBranch}`, {
|
|
555
|
-
cwd: WORKING_DIRECTORY,
|
|
556
|
-
stdio: 'pipe'
|
|
557
|
-
});
|
|
558
|
-
// Add to .gitignore if needed
|
|
559
|
-
try {
|
|
560
|
-
const gitignore = execSync(`cat "${WORKING_DIRECTORY}/.gitignore"`, {
|
|
561
|
-
encoding: 'utf-8',
|
|
562
|
-
stdio: 'pipe'
|
|
563
|
-
});
|
|
564
|
-
if (!gitignore.includes('worktrees/')) {
|
|
565
|
-
execSync(`echo "worktrees/" >> "${WORKING_DIRECTORY}/.gitignore"`, {
|
|
566
|
-
cwd: WORKING_DIRECTORY,
|
|
567
|
-
stdio: 'pipe'
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
catch {
|
|
572
|
-
// .gitignore might not exist
|
|
573
|
-
}
|
|
574
|
-
return {
|
|
575
|
-
content: [
|
|
576
|
-
{
|
|
577
|
-
type: 'text',
|
|
578
|
-
text: `✅ Created worktree at: ${worktreeDir}\n✅ Working on branch: ${branchName}\n✅ Ready for implementation`,
|
|
579
|
-
},
|
|
580
|
-
],
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
catch (error) {
|
|
584
|
-
return {
|
|
585
|
-
content: [
|
|
586
|
-
{
|
|
587
|
-
type: 'text',
|
|
588
|
-
text: `⚠️ Could not create worktree automatically: ${error.message}\n\nPlease create manually:\ngit worktree add worktrees/ticket-${ticketId} -b ${branchName} ${baseBranch}`,
|
|
589
|
-
},
|
|
590
|
-
],
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
case 'git_remove_worktree': {
|
|
595
|
-
if (!isGitAvailable()) {
|
|
596
|
-
return {
|
|
597
|
-
content: [
|
|
598
|
-
{
|
|
599
|
-
type: 'text',
|
|
600
|
-
text: `⚠️ Git is not available in this environment. Please remove the worktree manually:\n\ngit worktree remove worktrees/ticket-${args.ticketId}`,
|
|
601
|
-
},
|
|
602
|
-
],
|
|
603
|
-
};
|
|
604
|
-
}
|
|
605
|
-
const ticketId = args.ticketId;
|
|
606
|
-
const worktreeDir = `${WORKING_DIRECTORY}/worktrees/ticket-${ticketId}`;
|
|
607
|
-
try {
|
|
608
|
-
execSync(`git worktree remove "${worktreeDir}"`, {
|
|
609
|
-
cwd: WORKING_DIRECTORY,
|
|
610
|
-
stdio: 'pipe'
|
|
611
|
-
});
|
|
612
|
-
return {
|
|
613
|
-
content: [
|
|
614
|
-
{
|
|
615
|
-
type: 'text',
|
|
616
|
-
text: `✅ Worktree removed for ticket #${ticketId}`,
|
|
617
|
-
},
|
|
618
|
-
],
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
catch (error) {
|
|
622
|
-
return {
|
|
623
|
-
content: [
|
|
624
|
-
{
|
|
625
|
-
type: 'text',
|
|
626
|
-
text: `⚠️ Could not remove worktree automatically: ${error.message}\n\nPlease remove manually:\ngit worktree remove worktrees/ticket-${ticketId}`,
|
|
627
|
-
},
|
|
628
|
-
],
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
case 'github_create_pr': {
|
|
633
|
-
if (!isGhAvailable()) {
|
|
634
|
-
return {
|
|
635
|
-
content: [
|
|
636
|
-
{
|
|
637
|
-
type: 'text',
|
|
638
|
-
text: '⚠️ GitHub CLI (gh) is not installed or available. Please install it from https://cli.github.com/ and ensure you are authenticated with `gh auth login`.',
|
|
639
|
-
},
|
|
640
|
-
],
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
if (!GITHUB_REPO) {
|
|
644
|
-
return {
|
|
645
|
-
content: [
|
|
646
|
-
{
|
|
647
|
-
type: 'text',
|
|
648
|
-
text: '⚠️ Could not detect GitHub repository. Please ensure you are in a Git repository connected to GitHub, or set GITHUB_REPO environment variable.',
|
|
649
|
-
},
|
|
650
|
-
],
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
const title = args.title;
|
|
654
|
-
const body = args.body;
|
|
655
|
-
const ticketId = args.ticketId;
|
|
656
|
-
const branch = args.branch || '';
|
|
657
|
-
const baseBranch = args.baseBranch || 'master';
|
|
658
|
-
const draft = args.draft || false;
|
|
659
|
-
try {
|
|
660
|
-
// If ticketId is provided, add FreeScout link to the body
|
|
661
|
-
let enhancedBody = body;
|
|
662
|
-
if (ticketId) {
|
|
663
|
-
enhancedBody = `${body}\n\n---\n\nFreeScout Ticket: ${FREESCOUT_URL}/conversation/${ticketId}`;
|
|
664
|
-
}
|
|
665
|
-
// Create the PR using GitHub CLI (no token needed - gh handles auth)
|
|
666
|
-
const args_array = ['pr', 'create'];
|
|
667
|
-
if (GITHUB_REPO) {
|
|
668
|
-
args_array.push('--repo', GITHUB_REPO);
|
|
669
|
-
}
|
|
670
|
-
args_array.push('--title', title);
|
|
671
|
-
args_array.push('--body', enhancedBody);
|
|
672
|
-
args_array.push('--base', baseBranch);
|
|
673
|
-
if (draft) {
|
|
674
|
-
args_array.push('--draft');
|
|
675
|
-
}
|
|
676
|
-
if (branch) {
|
|
677
|
-
args_array.push('--head', branch);
|
|
678
|
-
}
|
|
679
|
-
const result = execSync(`gh ${args_array.map(arg => `"${arg.replace(/"/g, '\\"')}"`).join(' ')}`, {
|
|
680
|
-
cwd: WORKING_DIRECTORY,
|
|
681
|
-
encoding: 'utf-8',
|
|
682
|
-
stdio: 'pipe'
|
|
683
|
-
}).trim();
|
|
684
|
-
return {
|
|
685
|
-
content: [
|
|
686
|
-
{
|
|
687
|
-
type: 'text',
|
|
688
|
-
text: `✅ Pull request created successfully!\n\n${result}`,
|
|
689
|
-
},
|
|
690
|
-
],
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
catch (error) {
|
|
694
|
-
// Handle authentication errors
|
|
695
|
-
if (error.message.includes('not authenticated') || error.message.includes('authentication')) {
|
|
696
|
-
return {
|
|
697
|
-
content: [
|
|
698
|
-
{
|
|
699
|
-
type: 'text',
|
|
700
|
-
text: '⚠️ GitHub CLI is not authenticated. Please run `gh auth login` to authenticate with GitHub.',
|
|
701
|
-
},
|
|
702
|
-
],
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
return {
|
|
706
|
-
content: [
|
|
707
|
-
{
|
|
708
|
-
type: 'text',
|
|
709
|
-
text: `⚠️ Failed to create PR: ${error.message}\n\nPlease ensure:\n1. You are authenticated: \`gh auth login\`\n2. You are in a GitHub repository\n3. Your branch is pushed to GitHub`,
|
|
710
|
-
},
|
|
711
|
-
],
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
case 'freescout_implement_ticket': {
|
|
716
|
-
const ticketId = api.parseTicketInput(args.ticket);
|
|
717
|
-
const conversation = await api.getConversation(ticketId, true);
|
|
718
|
-
const analysis = analyzer.analyzeConversation(conversation);
|
|
719
|
-
let worktreeInfo = '';
|
|
720
|
-
if (args.autoCreateWorktree !== false) {
|
|
721
|
-
if (!isGitAvailable()) {
|
|
722
|
-
const branchName = `fix/freescout-${ticketId}`;
|
|
723
|
-
worktreeInfo = `\n\n## Git Worktree (Manual Setup Required)\n⚠️ Git is not available in this environment. Please create manually:\n\`\`\`bash\ngit worktree add worktrees/ticket-${ticketId} -b ${branchName} master\n\`\`\``;
|
|
724
|
-
}
|
|
725
|
-
else {
|
|
726
|
-
try {
|
|
727
|
-
const branchName = `fix/freescout-${ticketId}`;
|
|
728
|
-
const worktreeDir = `${WORKING_DIRECTORY}/worktrees/ticket-${ticketId}`;
|
|
729
|
-
execSync(`mkdir -p "${WORKING_DIRECTORY}/worktrees"`, {
|
|
730
|
-
cwd: WORKING_DIRECTORY,
|
|
731
|
-
stdio: 'pipe'
|
|
732
|
-
});
|
|
733
|
-
execSync(`git worktree add "${worktreeDir}" -b "${branchName}" master`, {
|
|
734
|
-
cwd: WORKING_DIRECTORY,
|
|
735
|
-
stdio: 'pipe'
|
|
736
|
-
});
|
|
737
|
-
worktreeInfo = `\n\n## Git Worktree Created\n- Branch: ${branchName}\n- Location: ${worktreeDir}`;
|
|
738
|
-
}
|
|
739
|
-
catch (error) {
|
|
740
|
-
const branchName = `fix/freescout-${ticketId}`;
|
|
741
|
-
worktreeInfo = `\n\n## Git Worktree (Manual Setup Required)\n⚠️ Could not create automatically: ${error.message}\n\nPlease create manually:\n\`\`\`bash\ngit worktree add worktrees/ticket-${ticketId} -b ${branchName} master\n\`\`\``;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
const plan = {
|
|
746
|
-
issue: analysis.issueDescription,
|
|
747
|
-
rootCause: analysis.rootCause || 'To be determined',
|
|
748
|
-
solution: analysis.suggestedSolution || 'To be implemented',
|
|
749
|
-
filesToModify: [],
|
|
750
|
-
alternativeApproaches: [],
|
|
751
|
-
hasBreakingChanges: false,
|
|
752
|
-
};
|
|
753
|
-
const output = `# FreeScout Ticket #${ticketId} Implementation Plan
|
|
754
|
-
|
|
755
|
-
## Customer Information
|
|
756
|
-
- Name: ${analysis.customerName}
|
|
757
|
-
- Email: ${analysis.customerEmail}
|
|
758
|
-
|
|
759
|
-
## Issue Analysis
|
|
760
|
-
- **Is Bug**: ${analysis.isBug ? 'Yes' : 'No'}
|
|
761
|
-
- **Is Third-Party Issue**: ${analysis.isThirdPartyIssue ? 'Yes' : 'No'}
|
|
762
|
-
- **Tested by Team**: ${analysis.testedByTeam ? 'Yes' : 'No'}
|
|
763
|
-
- **Reproducible**: ${analysis.isReproducible ? 'Yes' : 'No'}
|
|
764
|
-
|
|
765
|
-
## Issue Description
|
|
766
|
-
${analysis.issueDescription}
|
|
767
|
-
|
|
768
|
-
## Root Cause
|
|
769
|
-
${plan.rootCause}
|
|
770
|
-
|
|
771
|
-
## Proposed Solution
|
|
772
|
-
${plan.solution}
|
|
773
|
-
|
|
774
|
-
${analysis.codeSnippets.length > 0 ? `## Code Snippets from Ticket\n${analysis.codeSnippets.join('\n\n')}` : ''}
|
|
775
|
-
|
|
776
|
-
${analysis.errorMessages.length > 0 ? `## Error Messages\n${analysis.errorMessages.join('\n')}` : ''}
|
|
777
|
-
|
|
778
|
-
${analysis.hasAttachments ? `## Attachments\n${analysis.attachments.join('\n')}` : ''}
|
|
779
|
-
|
|
780
|
-
${args.additionalContext ? `## Additional Context\n${args.additionalContext}` : ''}
|
|
781
|
-
|
|
782
|
-
${worktreeInfo}
|
|
783
|
-
|
|
784
|
-
${GITHUB_REPO ? `## GitHub Repository\n- Repository: ${GITHUB_REPO}\n- Ready for PR creation with \`github_create_pr\` tool\n- Requires: GitHub CLI (\`gh\`) installed and authenticated` : '## GitHub Repository\n- ⚠️ No GitHub repository detected\n- Install GitHub CLI: \`gh\` and run \`gh auth login\`\n- Or set GITHUB_REPO environment variable'}
|
|
785
|
-
|
|
786
|
-
## Next Steps
|
|
787
|
-
1. Review the analysis above
|
|
788
|
-
2. ${analysis.isBug ? 'Implement the fix in the worktree' : 'Draft an explanatory reply'}
|
|
789
|
-
3. Test the changes
|
|
790
|
-
4. Create a pull request${GITHUB_REPO ? ' using `github_create_pr` tool' : ''}
|
|
791
|
-
5. Update the FreeScout ticket`;
|
|
792
|
-
return {
|
|
793
|
-
content: [
|
|
794
|
-
{
|
|
795
|
-
type: 'text',
|
|
796
|
-
text: output,
|
|
797
|
-
},
|
|
798
|
-
],
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
default:
|
|
802
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
catch (error) {
|
|
806
|
-
return {
|
|
807
|
-
content: [
|
|
808
|
-
{
|
|
809
|
-
type: 'text',
|
|
810
|
-
text: `Error: ${error.message}`,
|
|
811
|
-
},
|
|
812
|
-
],
|
|
813
|
-
};
|
|
814
|
-
}
|
|
259
|
+
// Tool 8: Get Mailboxes
|
|
260
|
+
server.registerTool('freescout_get_mailboxes', {
|
|
261
|
+
title: 'Get Mailboxes',
|
|
262
|
+
description: 'Get list of available mailboxes',
|
|
263
|
+
inputSchema: {},
|
|
264
|
+
outputSchema: {
|
|
265
|
+
mailboxes: z.array(z.any()),
|
|
266
|
+
},
|
|
267
|
+
}, async () => {
|
|
268
|
+
const mailboxes = await api.getMailboxes();
|
|
269
|
+
const output = { mailboxes };
|
|
270
|
+
return {
|
|
271
|
+
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
272
|
+
structuredContent: output,
|
|
273
|
+
};
|
|
815
274
|
});
|
|
816
275
|
// Start the server
|
|
817
276
|
async function main() {
|
|
818
277
|
const transport = new StdioServerTransport();
|
|
819
278
|
await server.connect(transport);
|
|
820
|
-
console.error('FreeScout MCP Server running...');
|
|
279
|
+
console.error('FreeScout MCP Server v2.0.0 running...');
|
|
821
280
|
}
|
|
822
281
|
main().catch((error) => {
|
|
823
282
|
console.error('Server error:', error);
|