freshservice-mcp 0.1.1 → 0.2.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/README.md +23 -2
- package/dist/index.js +427 -17
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
|
|
8
|
-
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for [Freshservice](https://freshservice.com). Manage your ITSM tickets, conversations, and agents directly from any MCP-compatible AI client (Kiro, Claude Desktop, Cursor, etc.).
|
|
8
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for [Freshservice](https://freshservice.com). Manage your ITSM tickets, changes, conversations, and agents directly from any MCP-compatible AI client (Kiro, Claude Desktop, Cursor, etc.).
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
@@ -15,6 +15,9 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for [Fr
|
|
|
15
15
|
- ✏️ **Create tickets** — with full field support
|
|
16
16
|
- 🔄 **Update tickets** — status, priority, assignment, tags
|
|
17
17
|
- 📝 **Add notes or replies** — private notes or public replies
|
|
18
|
+
- 🗂️ **Manage changes** — list, inspect, create, and update Changes
|
|
19
|
+
- 🔗 **Associate tickets with changes** — supports both Freshservice relationship directions
|
|
20
|
+
- 📝 **Add change notes** — add HTML-formatted notes to Changes
|
|
18
21
|
- 👤 **Agent lookup** — get your own profile or search by email
|
|
19
22
|
- 🔒 **Read-only mode** — disable all write operations with one env var
|
|
20
23
|
|
|
@@ -69,7 +72,7 @@ Reconnect MCP servers (or reload the window). You should now have access to all
|
|
|
69
72
|
|
|
70
73
|
### Read-only mode
|
|
71
74
|
|
|
72
|
-
Set `FRESHSERVICE_READONLY` to `"true"` to prevent any modifications. In this mode,
|
|
75
|
+
Set `FRESHSERVICE_READONLY` to `"true"` to prevent any modifications. In this mode, only ticket, Change, and agent read tools are registered; all create, update, association, note, and reply tools are omitted. Recommended for shared environments or autonomous agent setups against production instances.
|
|
73
76
|
|
|
74
77
|
## Available Tools
|
|
75
78
|
|
|
@@ -81,6 +84,12 @@ Set `FRESHSERVICE_READONLY` to `"true"` to prevent any modifications. In this mo
|
|
|
81
84
|
| `create_ticket` | Create a new ticket |
|
|
82
85
|
| `update_ticket` | Update ticket fields |
|
|
83
86
|
| `add_ticket_note` | Add a private note or public reply |
|
|
87
|
+
| `list_changes` | List Changes with query/view filters, sorting, and pagination |
|
|
88
|
+
| `get_change` | Get full details of a Change |
|
|
89
|
+
| `create_change` | Create a Change |
|
|
90
|
+
| `update_change` | Update Change fields |
|
|
91
|
+
| `associate_tickets_to_change` | Associate one or more tickets with a Change |
|
|
92
|
+
| `add_change_note` | Add a note to a Change |
|
|
84
93
|
| `get_me` | Get the authenticated agent's profile |
|
|
85
94
|
| `list_agents` | List agents, optionally filter by email |
|
|
86
95
|
|
|
@@ -186,6 +195,18 @@ Useful optional fields:
|
|
|
186
195
|
- `responder_id` — assign directly to an agent
|
|
187
196
|
- `tags` — categorization
|
|
188
197
|
|
|
198
|
+
## Change Management
|
|
199
|
+
|
|
200
|
+
Freshservice Changes use numeric values for their standard fields:
|
|
201
|
+
|
|
202
|
+
- Status: 1=Open, 2=Planning, 3=Awaiting Approval, 4=Pending Release, 5=Pending Review, 6=Closed
|
|
203
|
+
- Change type: 1=Minor, 2=Standard, 3=Major, 4=Emergency
|
|
204
|
+
- Priority: 1=Low, 2=Medium, 3=High, 4=Urgent
|
|
205
|
+
- Impact: 1=Low, 2=Medium, 3=High
|
|
206
|
+
- Risk: 1=Low, 2=Medium, 3=High, 4=Very High
|
|
207
|
+
|
|
208
|
+
`associate_tickets_to_change` uses the association shape documented by Freshservice's Ticket API. The default relationship, `change_initiated_by_ticket`, means the ticket initiated the Change. Use `association_type: "change_initiating_ticket"` when the Change initiated the ticket. `change_id` is placed in the documented nested `display_id` field.
|
|
209
|
+
|
|
189
210
|
## Local Development
|
|
190
211
|
|
|
191
212
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -4581,9 +4581,9 @@ var require_codegen = __commonJS({
|
|
|
4581
4581
|
}
|
|
4582
4582
|
};
|
|
4583
4583
|
var Label = class extends Node {
|
|
4584
|
-
constructor(
|
|
4584
|
+
constructor(label3) {
|
|
4585
4585
|
super();
|
|
4586
|
-
this.label =
|
|
4586
|
+
this.label = label3;
|
|
4587
4587
|
this.names = {};
|
|
4588
4588
|
}
|
|
4589
4589
|
render({ _n }) {
|
|
@@ -4591,14 +4591,14 @@ var require_codegen = __commonJS({
|
|
|
4591
4591
|
}
|
|
4592
4592
|
};
|
|
4593
4593
|
var Break = class extends Node {
|
|
4594
|
-
constructor(
|
|
4594
|
+
constructor(label3) {
|
|
4595
4595
|
super();
|
|
4596
|
-
this.label =
|
|
4596
|
+
this.label = label3;
|
|
4597
4597
|
this.names = {};
|
|
4598
4598
|
}
|
|
4599
4599
|
render({ _n }) {
|
|
4600
|
-
const
|
|
4601
|
-
return `break${
|
|
4600
|
+
const label3 = this.label ? ` ${this.label}` : "";
|
|
4601
|
+
return `break${label3};` + _n;
|
|
4602
4602
|
}
|
|
4603
4603
|
};
|
|
4604
4604
|
var Throw = class extends Node {
|
|
@@ -5010,12 +5010,12 @@ var require_codegen = __commonJS({
|
|
|
5010
5010
|
return this._endBlockNode(For);
|
|
5011
5011
|
}
|
|
5012
5012
|
// `label` statement
|
|
5013
|
-
label(
|
|
5014
|
-
return this._leafNode(new Label(
|
|
5013
|
+
label(label3) {
|
|
5014
|
+
return this._leafNode(new Label(label3));
|
|
5015
5015
|
}
|
|
5016
5016
|
// `break` statement
|
|
5017
|
-
break(
|
|
5018
|
-
return this._leafNode(new Break(
|
|
5017
|
+
break(label3) {
|
|
5018
|
+
return this._leafNode(new Break(label3));
|
|
5019
5019
|
}
|
|
5020
5020
|
// `return` statement
|
|
5021
5021
|
return(value) {
|
|
@@ -11038,7 +11038,7 @@ async function fsRequest(config2, method, path, params = {}, body) {
|
|
|
11038
11038
|
if (!res.ok) {
|
|
11039
11039
|
const text = await res.text().catch(() => "");
|
|
11040
11040
|
throw new Error(
|
|
11041
|
-
`Freshservice API ${method} ${path} \u2192 ${res.status}: ${text.slice(0,
|
|
11041
|
+
`Freshservice API ${method} ${path} \u2192 ${res.status}: ${text.slice(0, 2e3)}`
|
|
11042
11042
|
);
|
|
11043
11043
|
}
|
|
11044
11044
|
if (res.status === 204) return void 0;
|
|
@@ -11127,6 +11127,73 @@ async function replyToTicket(config2, ticketId, payload) {
|
|
|
11127
11127
|
);
|
|
11128
11128
|
return data.conversation;
|
|
11129
11129
|
}
|
|
11130
|
+
function withChangePlan(payload) {
|
|
11131
|
+
const { change_plan, ...rest } = payload;
|
|
11132
|
+
return change_plan === void 0 ? rest : { ...rest, planning_fields: { change_plan } };
|
|
11133
|
+
}
|
|
11134
|
+
async function listChanges(config2, options = {}) {
|
|
11135
|
+
const limit = options.limit ?? 100;
|
|
11136
|
+
const changes = [];
|
|
11137
|
+
const params = { per_page: "100" };
|
|
11138
|
+
if (options.query) params.query = `"${options.query}"`;
|
|
11139
|
+
if (options.view) params.view = options.view;
|
|
11140
|
+
if (options.updated_since) params.updated_since = options.updated_since;
|
|
11141
|
+
if (options.workspace_id !== void 0) params.workspace_id = String(options.workspace_id);
|
|
11142
|
+
if (options.order_by) params.order_by = options.order_by;
|
|
11143
|
+
if (options.order_type) params.order_type = options.order_type;
|
|
11144
|
+
let page = options.page ?? 1;
|
|
11145
|
+
while (changes.length < limit) {
|
|
11146
|
+
params.page = String(page);
|
|
11147
|
+
const data = await fsGet(config2, "changes", params);
|
|
11148
|
+
const batch = data.changes ?? [];
|
|
11149
|
+
if (batch.length === 0) break;
|
|
11150
|
+
for (const change of batch) {
|
|
11151
|
+
changes.push(change);
|
|
11152
|
+
if (changes.length >= limit) break;
|
|
11153
|
+
}
|
|
11154
|
+
if (batch.length < 100) break;
|
|
11155
|
+
page++;
|
|
11156
|
+
}
|
|
11157
|
+
return changes;
|
|
11158
|
+
}
|
|
11159
|
+
async function getChange(config2, changeId, include) {
|
|
11160
|
+
const params = include ? { include } : {};
|
|
11161
|
+
const data = await fsGet(config2, `changes/${changeId}`, params);
|
|
11162
|
+
return data.change;
|
|
11163
|
+
}
|
|
11164
|
+
async function createChange(config2, payload) {
|
|
11165
|
+
const data = await fsPost(
|
|
11166
|
+
config2,
|
|
11167
|
+
"changes",
|
|
11168
|
+
withChangePlan(payload)
|
|
11169
|
+
);
|
|
11170
|
+
return data.change;
|
|
11171
|
+
}
|
|
11172
|
+
async function updateChange(config2, changeId, payload) {
|
|
11173
|
+
const data = await fsPut(
|
|
11174
|
+
config2,
|
|
11175
|
+
`changes/${changeId}`,
|
|
11176
|
+
withChangePlan(payload)
|
|
11177
|
+
);
|
|
11178
|
+
return data.change;
|
|
11179
|
+
}
|
|
11180
|
+
async function associateTicketsToChange(config2, changeDisplayId, ticketIds, associationType = "change_initiated_by_ticket") {
|
|
11181
|
+
return Promise.all(
|
|
11182
|
+
ticketIds.map(
|
|
11183
|
+
(ticketId) => updateTicket(config2, ticketId, {
|
|
11184
|
+
[associationType]: { display_id: changeDisplayId }
|
|
11185
|
+
})
|
|
11186
|
+
)
|
|
11187
|
+
);
|
|
11188
|
+
}
|
|
11189
|
+
async function addChangeNote(config2, changeId, payload) {
|
|
11190
|
+
const data = await fsPost(
|
|
11191
|
+
config2,
|
|
11192
|
+
`changes/${changeId}/notes`,
|
|
11193
|
+
payload
|
|
11194
|
+
);
|
|
11195
|
+
return data.note;
|
|
11196
|
+
}
|
|
11130
11197
|
async function getMe(config2) {
|
|
11131
11198
|
const data = await fsGet(config2, "agents/me");
|
|
11132
11199
|
return data.agent;
|
|
@@ -11139,7 +11206,7 @@ async function listAgents(config2, options = {}) {
|
|
|
11139
11206
|
const data = await fsGet(config2, "agents", params);
|
|
11140
11207
|
return data.agents ?? [];
|
|
11141
11208
|
}
|
|
11142
|
-
var DEFAULT_STATUS_MAP, PRIORITY_MAP;
|
|
11209
|
+
var DEFAULT_STATUS_MAP, PRIORITY_MAP, CHANGE_PRIORITY_MAP, CHANGE_STATUS_MAP, CHANGE_TYPE_MAP, CHANGE_IMPACT_MAP, CHANGE_RISK_MAP;
|
|
11143
11210
|
var init_freshservice_client = __esm({
|
|
11144
11211
|
"src/freshservice-client.ts"() {
|
|
11145
11212
|
"use strict";
|
|
@@ -11155,6 +11222,32 @@ var init_freshservice_client = __esm({
|
|
|
11155
11222
|
3: "High",
|
|
11156
11223
|
4: "Urgent"
|
|
11157
11224
|
};
|
|
11225
|
+
CHANGE_PRIORITY_MAP = PRIORITY_MAP;
|
|
11226
|
+
CHANGE_STATUS_MAP = {
|
|
11227
|
+
1: "Open",
|
|
11228
|
+
2: "Planning",
|
|
11229
|
+
3: "Awaiting Approval",
|
|
11230
|
+
4: "Pending Release",
|
|
11231
|
+
5: "Pending Review",
|
|
11232
|
+
6: "Closed"
|
|
11233
|
+
};
|
|
11234
|
+
CHANGE_TYPE_MAP = {
|
|
11235
|
+
1: "Minor",
|
|
11236
|
+
2: "Standard",
|
|
11237
|
+
3: "Major",
|
|
11238
|
+
4: "Emergency"
|
|
11239
|
+
};
|
|
11240
|
+
CHANGE_IMPACT_MAP = {
|
|
11241
|
+
1: "Low",
|
|
11242
|
+
2: "Medium",
|
|
11243
|
+
3: "High"
|
|
11244
|
+
};
|
|
11245
|
+
CHANGE_RISK_MAP = {
|
|
11246
|
+
1: "Low",
|
|
11247
|
+
2: "Medium",
|
|
11248
|
+
3: "High",
|
|
11249
|
+
4: "Very High"
|
|
11250
|
+
};
|
|
11158
11251
|
}
|
|
11159
11252
|
});
|
|
11160
11253
|
|
|
@@ -11279,6 +11372,94 @@ var init_tickets_readonly = __esm({
|
|
|
11279
11372
|
}
|
|
11280
11373
|
});
|
|
11281
11374
|
|
|
11375
|
+
// src/tools/changes-readonly.ts
|
|
11376
|
+
var changes_readonly_exports = {};
|
|
11377
|
+
__export(changes_readonly_exports, {
|
|
11378
|
+
registerReadOnlyChangeTools: () => registerReadOnlyChangeTools
|
|
11379
|
+
});
|
|
11380
|
+
function label2(value, values) {
|
|
11381
|
+
return values[value] ?? String(value);
|
|
11382
|
+
}
|
|
11383
|
+
function normalizeChange2(change) {
|
|
11384
|
+
return {
|
|
11385
|
+
id: change.id,
|
|
11386
|
+
subject: change.subject,
|
|
11387
|
+
status: label2(change.status, CHANGE_STATUS_MAP),
|
|
11388
|
+
priority: label2(change.priority, CHANGE_PRIORITY_MAP),
|
|
11389
|
+
impact: label2(change.impact, CHANGE_IMPACT_MAP),
|
|
11390
|
+
risk: label2(change.risk, CHANGE_RISK_MAP),
|
|
11391
|
+
change_type: label2(change.change_type, CHANGE_TYPE_MAP),
|
|
11392
|
+
requester_id: change.requester_id,
|
|
11393
|
+
agent_id: change.agent_id ?? null,
|
|
11394
|
+
group_id: change.group_id ?? null,
|
|
11395
|
+
planned_start_date: change.planned_start_date ?? null,
|
|
11396
|
+
planned_end_date: change.planned_end_date ?? null,
|
|
11397
|
+
created_at: change.created_at,
|
|
11398
|
+
updated_at: change.updated_at,
|
|
11399
|
+
url: `https://${process.env.FRESHSERVICE_DOMAIN}/helpdesk/changes/${change.id}`
|
|
11400
|
+
};
|
|
11401
|
+
}
|
|
11402
|
+
function registerReadOnlyChangeTools(server, config2) {
|
|
11403
|
+
server.registerTool(
|
|
11404
|
+
"list_changes",
|
|
11405
|
+
{
|
|
11406
|
+
title: "List Changes",
|
|
11407
|
+
description: "List Freshservice Changes with filtering, sorting and pagination.",
|
|
11408
|
+
inputSchema: external_exports.object({
|
|
11409
|
+
query: external_exports.string().max(512).optional(),
|
|
11410
|
+
view: external_exports.string().optional(),
|
|
11411
|
+
updated_since: external_exports.string().optional(),
|
|
11412
|
+
workspace_id: external_exports.number().int().nonnegative().optional(),
|
|
11413
|
+
order_by: external_exports.string().optional(),
|
|
11414
|
+
order_type: external_exports.enum(["asc", "desc"]).optional(),
|
|
11415
|
+
limit: external_exports.number().int().min(1).max(500).optional()
|
|
11416
|
+
}).refine(({ query, view }) => !(query && view), {
|
|
11417
|
+
message: "query and view cannot be used together"
|
|
11418
|
+
})
|
|
11419
|
+
},
|
|
11420
|
+
async (args) => {
|
|
11421
|
+
const changes = await listChanges(config2, args);
|
|
11422
|
+
if (changes.length === 0) {
|
|
11423
|
+
return { content: [{ type: "text", text: "No changes found." }] };
|
|
11424
|
+
}
|
|
11425
|
+
const normalized = changes.map(normalizeChange2);
|
|
11426
|
+
return {
|
|
11427
|
+
content: [{
|
|
11428
|
+
type: "text",
|
|
11429
|
+
text: JSON.stringify({ count: normalized.length, changes: normalized }, null, 2)
|
|
11430
|
+
}]
|
|
11431
|
+
};
|
|
11432
|
+
}
|
|
11433
|
+
);
|
|
11434
|
+
server.registerTool(
|
|
11435
|
+
"get_change",
|
|
11436
|
+
{
|
|
11437
|
+
title: "Get Change",
|
|
11438
|
+
description: "Retrieve full details of a Freshservice Change by ID.",
|
|
11439
|
+
inputSchema: external_exports.object({
|
|
11440
|
+
change_id: external_exports.number().int().positive(),
|
|
11441
|
+
include: external_exports.string().optional()
|
|
11442
|
+
})
|
|
11443
|
+
},
|
|
11444
|
+
async ({ change_id, include }) => {
|
|
11445
|
+
const change = await getChange(config2, change_id, include);
|
|
11446
|
+
const result = {
|
|
11447
|
+
...normalizeChange2(change),
|
|
11448
|
+
description: change.description_text?.trim() ?? stripHtml(change.description ?? ""),
|
|
11449
|
+
change_plan: change.planning_fields?.change_plan ? stripHtml(change.planning_fields.change_plan) : null
|
|
11450
|
+
};
|
|
11451
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
11452
|
+
}
|
|
11453
|
+
);
|
|
11454
|
+
}
|
|
11455
|
+
var init_changes_readonly = __esm({
|
|
11456
|
+
"src/tools/changes-readonly.ts"() {
|
|
11457
|
+
"use strict";
|
|
11458
|
+
init_esm();
|
|
11459
|
+
init_freshservice_client();
|
|
11460
|
+
}
|
|
11461
|
+
});
|
|
11462
|
+
|
|
11282
11463
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
|
|
11283
11464
|
init_esm();
|
|
11284
11465
|
|
|
@@ -21266,6 +21447,7 @@ var StdioServerTransport = class {
|
|
|
21266
21447
|
// src/tools/tickets.ts
|
|
21267
21448
|
init_esm();
|
|
21268
21449
|
init_freshservice_client();
|
|
21450
|
+
var HTML_FIELD_HINT = 'HTML is supported \u2014 pass RAW tags (<p>, <b>, <ul>, <li>, <code>). Do NOT HTML-escape the markup: the value is stored verbatim, so <p> renders as the literal text "<p>" instead of a paragraph. Use entities only for characters that must appear literally in the content.';
|
|
21269
21451
|
function statusLabel(status, statusMap) {
|
|
21270
21452
|
return statusMap[status] ?? String(status);
|
|
21271
21453
|
}
|
|
@@ -21387,7 +21569,7 @@ function registerTicketTools(server, config2) {
|
|
|
21387
21569
|
description: "Create a new Freshservice ticket. Either `email` or `requester_id` must be provided.",
|
|
21388
21570
|
inputSchema: external_exports.object({
|
|
21389
21571
|
subject: external_exports.string().min(1).describe("Ticket subject / title"),
|
|
21390
|
-
description: external_exports.string().optional().describe(
|
|
21572
|
+
description: external_exports.string().optional().describe(`Ticket description. ${HTML_FIELD_HINT}`),
|
|
21391
21573
|
email: external_exports.string().email().optional().describe("Requester email address"),
|
|
21392
21574
|
requester_id: external_exports.number().int().positive().optional().describe("Requester agent/user ID"),
|
|
21393
21575
|
priority: external_exports.enum(["1", "2", "3", "4"]).optional().describe("Priority: 1=Low, 2=Medium, 3=High, 4=Urgent (default: 2)"),
|
|
@@ -21396,10 +21578,14 @@ function registerTicketTools(server, config2) {
|
|
|
21396
21578
|
responder_id: external_exports.number().int().positive().optional().describe("Agent ID to assign to"),
|
|
21397
21579
|
tags: external_exports.array(external_exports.string()).optional().describe("List of tags"),
|
|
21398
21580
|
type: external_exports.string().optional().describe("Ticket type, e.g. Incident, Service Request"),
|
|
21581
|
+
category: external_exports.string().optional().describe(
|
|
21582
|
+
"Ticket category. Required on Freshservice instances where the category field is mandatory (create_ticket fails with a 400 validation error listing the allowed values if omitted on such instances)."
|
|
21583
|
+
),
|
|
21584
|
+
sub_category: external_exports.string().optional().describe("Ticket sub-category. Only meaningful together with `category`."),
|
|
21399
21585
|
cc_emails: external_exports.array(external_exports.string().email()).optional().describe("CC email addresses")
|
|
21400
21586
|
})
|
|
21401
21587
|
},
|
|
21402
|
-
async ({ subject, description, email: email2, requester_id, priority, status, group_id, responder_id, tags, type, cc_emails }) => {
|
|
21588
|
+
async ({ subject, description, email: email2, requester_id, priority, status, group_id, responder_id, tags, type, category, sub_category, cc_emails }) => {
|
|
21403
21589
|
const ticket = await createTicket(config2, {
|
|
21404
21590
|
subject,
|
|
21405
21591
|
description,
|
|
@@ -21411,6 +21597,8 @@ function registerTicketTools(server, config2) {
|
|
|
21411
21597
|
responder_id,
|
|
21412
21598
|
tags,
|
|
21413
21599
|
type,
|
|
21600
|
+
category,
|
|
21601
|
+
sub_category,
|
|
21414
21602
|
cc_emails
|
|
21415
21603
|
});
|
|
21416
21604
|
const result = normalizeTicket(ticket, DEFAULT_STATUS_MAP);
|
|
@@ -21434,7 +21622,7 @@ ${JSON.stringify(result, null, 2)}`
|
|
|
21434
21622
|
inputSchema: external_exports.object({
|
|
21435
21623
|
ticket_id: external_exports.number().int().positive().describe("Freshservice ticket ID"),
|
|
21436
21624
|
subject: external_exports.string().optional().describe("New subject"),
|
|
21437
|
-
description: external_exports.string().optional().describe(
|
|
21625
|
+
description: external_exports.string().optional().describe(`New description. ${HTML_FIELD_HINT}`),
|
|
21438
21626
|
priority: external_exports.enum(["1", "2", "3", "4"]).optional().describe("Priority: 1=Low, 2=Medium, 3=High, 4=Urgent"),
|
|
21439
21627
|
status: external_exports.number().int().optional().describe("New status code (e.g. 2=Open, 3=Pending, 4=Resolved, 5=Closed)"),
|
|
21440
21628
|
group_id: external_exports.number().int().positive().optional().describe("Agent group ID"),
|
|
@@ -21474,7 +21662,7 @@ ${JSON.stringify(result, null, 2)}`
|
|
|
21474
21662
|
description: "Add a private note or public reply to a Freshservice ticket.",
|
|
21475
21663
|
inputSchema: external_exports.object({
|
|
21476
21664
|
ticket_id: external_exports.number().int().positive().describe("Freshservice ticket ID"),
|
|
21477
|
-
body: external_exports.string().min(1).describe(
|
|
21665
|
+
body: external_exports.string().min(1).describe(`Note or reply body. ${HTML_FIELD_HINT}`),
|
|
21478
21666
|
private: external_exports.boolean().optional().describe("true = private note (default), false = public reply visible to requester"),
|
|
21479
21667
|
notify_emails: external_exports.array(external_exports.string().email()).optional().describe("Additional email addresses to notify")
|
|
21480
21668
|
})
|
|
@@ -21505,6 +21693,225 @@ ${JSON.stringify(result, null, 2)}`
|
|
|
21505
21693
|
);
|
|
21506
21694
|
}
|
|
21507
21695
|
|
|
21696
|
+
// src/tools/changes.ts
|
|
21697
|
+
init_esm();
|
|
21698
|
+
init_freshservice_client();
|
|
21699
|
+
function label(value, values) {
|
|
21700
|
+
return values[value] ?? String(value);
|
|
21701
|
+
}
|
|
21702
|
+
function normalizeChange(change) {
|
|
21703
|
+
return {
|
|
21704
|
+
id: change.id,
|
|
21705
|
+
subject: change.subject,
|
|
21706
|
+
status: label(change.status, CHANGE_STATUS_MAP),
|
|
21707
|
+
priority: label(change.priority, CHANGE_PRIORITY_MAP),
|
|
21708
|
+
impact: label(change.impact, CHANGE_IMPACT_MAP),
|
|
21709
|
+
risk: label(change.risk, CHANGE_RISK_MAP),
|
|
21710
|
+
change_type: label(change.change_type, CHANGE_TYPE_MAP),
|
|
21711
|
+
requester_id: change.requester_id,
|
|
21712
|
+
agent_id: change.agent_id ?? null,
|
|
21713
|
+
group_id: change.group_id ?? null,
|
|
21714
|
+
planned_start_date: change.planned_start_date ?? null,
|
|
21715
|
+
planned_end_date: change.planned_end_date ?? null,
|
|
21716
|
+
created_at: change.created_at,
|
|
21717
|
+
updated_at: change.updated_at,
|
|
21718
|
+
url: `https://${process.env.FRESHSERVICE_DOMAIN}/helpdesk/changes/${change.id}`
|
|
21719
|
+
};
|
|
21720
|
+
}
|
|
21721
|
+
var listChangesSchema = external_exports.object({
|
|
21722
|
+
query: external_exports.string().max(512).optional().describe('Freshservice query, e.g. "priority:4 OR priority:3"'),
|
|
21723
|
+
view: external_exports.string().optional().describe("Default view name or custom Change view ID; cannot be combined with query"),
|
|
21724
|
+
updated_since: external_exports.string().optional().describe("Return Changes updated since this UTC date or timestamp"),
|
|
21725
|
+
workspace_id: external_exports.number().int().nonnegative().optional().describe("Workspace ID; 0 requests Changes across all accessible workspaces"),
|
|
21726
|
+
order_by: external_exports.string().optional().describe("Field to sort by, e.g. priority or updated_at"),
|
|
21727
|
+
order_type: external_exports.enum(["asc", "desc"]).optional().describe("Sort direction (default: desc)"),
|
|
21728
|
+
limit: external_exports.number().int().min(1).max(500).optional().describe("Maximum results (default: 100)")
|
|
21729
|
+
}).refine(({ query, view }) => !(query && view), {
|
|
21730
|
+
message: "query and view cannot be used together"
|
|
21731
|
+
});
|
|
21732
|
+
var changeFields = {
|
|
21733
|
+
subject: external_exports.string().min(1).optional().describe("Change subject"),
|
|
21734
|
+
description: external_exports.string().min(1).optional().describe(`Change description. ${HTML_FIELD_HINT}`),
|
|
21735
|
+
requester_id: external_exports.number().int().positive().optional().describe("Initiating requester ID"),
|
|
21736
|
+
priority: external_exports.enum(["1", "2", "3", "4"]).optional().describe("1=Low, 2=Medium, 3=High, 4=Urgent"),
|
|
21737
|
+
status: external_exports.enum(["1", "2", "3", "4", "5", "6"]).optional().describe(
|
|
21738
|
+
"1=Open, 2=Planning, 3=Awaiting Approval, 4=Pending Release, 5=Pending Review, 6=Closed"
|
|
21739
|
+
),
|
|
21740
|
+
impact: external_exports.enum(["1", "2", "3"]).optional().describe("1=Low, 2=Medium, 3=High"),
|
|
21741
|
+
risk: external_exports.enum(["1", "2", "3", "4"]).optional().describe("1=Low, 2=Medium, 3=High, 4=Very High"),
|
|
21742
|
+
change_type: external_exports.enum(["1", "2", "3", "4"]).optional().describe(
|
|
21743
|
+
"1=Minor, 2=Standard, 3=Major, 4=Emergency"
|
|
21744
|
+
),
|
|
21745
|
+
planned_start_date: external_exports.string().optional().describe("Planned start as an ISO 8601 UTC timestamp"),
|
|
21746
|
+
planned_end_date: external_exports.string().optional().describe("Planned end as an ISO 8601 UTC timestamp"),
|
|
21747
|
+
change_plan: external_exports.string().optional().describe(`Rollout plan. ${HTML_FIELD_HINT}`),
|
|
21748
|
+
group_id: external_exports.number().int().positive().optional().describe("Assigned agent group ID"),
|
|
21749
|
+
agent_id: external_exports.number().int().positive().optional().describe("Assigned agent ID")
|
|
21750
|
+
};
|
|
21751
|
+
function payloadFromArgs(args) {
|
|
21752
|
+
const payload = {};
|
|
21753
|
+
for (const [key, value] of Object.entries(args)) {
|
|
21754
|
+
if (value === void 0 || key === "change_id") continue;
|
|
21755
|
+
payload[key] = ["priority", "status", "impact", "risk", "change_type"].includes(key) ? Number(value) : value;
|
|
21756
|
+
}
|
|
21757
|
+
return payload;
|
|
21758
|
+
}
|
|
21759
|
+
function registerChangeTools(server, config2) {
|
|
21760
|
+
server.registerTool(
|
|
21761
|
+
"list_changes",
|
|
21762
|
+
{
|
|
21763
|
+
title: "List Changes",
|
|
21764
|
+
description: "List Freshservice Changes with filtering, sorting and pagination.",
|
|
21765
|
+
inputSchema: listChangesSchema
|
|
21766
|
+
},
|
|
21767
|
+
async (args) => {
|
|
21768
|
+
const changes = await listChanges(config2, args);
|
|
21769
|
+
if (changes.length === 0) {
|
|
21770
|
+
return { content: [{ type: "text", text: "No changes found." }] };
|
|
21771
|
+
}
|
|
21772
|
+
const normalized = changes.map(normalizeChange);
|
|
21773
|
+
return {
|
|
21774
|
+
content: [{
|
|
21775
|
+
type: "text",
|
|
21776
|
+
text: JSON.stringify({ count: normalized.length, changes: normalized }, null, 2)
|
|
21777
|
+
}]
|
|
21778
|
+
};
|
|
21779
|
+
}
|
|
21780
|
+
);
|
|
21781
|
+
server.registerTool(
|
|
21782
|
+
"get_change",
|
|
21783
|
+
{
|
|
21784
|
+
title: "Get Change",
|
|
21785
|
+
description: "Retrieve full details of a Freshservice Change by ID.",
|
|
21786
|
+
inputSchema: external_exports.object({
|
|
21787
|
+
change_id: external_exports.number().int().positive().describe("Freshservice Change ID"),
|
|
21788
|
+
include: external_exports.string().optional().describe("Extra details to embed, e.g. stats")
|
|
21789
|
+
})
|
|
21790
|
+
},
|
|
21791
|
+
async ({ change_id, include }) => {
|
|
21792
|
+
const change = await getChange(config2, change_id, include);
|
|
21793
|
+
const result = {
|
|
21794
|
+
...normalizeChange(change),
|
|
21795
|
+
description: change.description_text?.trim() ?? stripHtml(change.description ?? ""),
|
|
21796
|
+
change_plan: change.planning_fields?.change_plan ? stripHtml(change.planning_fields.change_plan) : null
|
|
21797
|
+
};
|
|
21798
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21799
|
+
}
|
|
21800
|
+
);
|
|
21801
|
+
server.registerTool(
|
|
21802
|
+
"create_change",
|
|
21803
|
+
{
|
|
21804
|
+
title: "Create Change",
|
|
21805
|
+
description: "Create a Freshservice Change with all standard mandatory fields.",
|
|
21806
|
+
inputSchema: external_exports.object({
|
|
21807
|
+
subject: changeFields.subject.unwrap(),
|
|
21808
|
+
description: changeFields.description.unwrap(),
|
|
21809
|
+
requester_id: changeFields.requester_id.unwrap(),
|
|
21810
|
+
priority: changeFields.priority.unwrap(),
|
|
21811
|
+
status: changeFields.status.unwrap(),
|
|
21812
|
+
impact: changeFields.impact.unwrap(),
|
|
21813
|
+
risk: changeFields.risk.unwrap(),
|
|
21814
|
+
change_type: changeFields.change_type.unwrap(),
|
|
21815
|
+
planned_start_date: changeFields.planned_start_date.unwrap(),
|
|
21816
|
+
planned_end_date: changeFields.planned_end_date.unwrap(),
|
|
21817
|
+
change_plan: changeFields.change_plan,
|
|
21818
|
+
group_id: changeFields.group_id,
|
|
21819
|
+
agent_id: changeFields.agent_id
|
|
21820
|
+
})
|
|
21821
|
+
},
|
|
21822
|
+
async (args) => {
|
|
21823
|
+
const change = await createChange(config2, payloadFromArgs(args));
|
|
21824
|
+
return {
|
|
21825
|
+
content: [{
|
|
21826
|
+
type: "text",
|
|
21827
|
+
text: `Change #${change.id} created.
|
|
21828
|
+
|
|
21829
|
+
${JSON.stringify(normalizeChange(change), null, 2)}`
|
|
21830
|
+
}]
|
|
21831
|
+
};
|
|
21832
|
+
}
|
|
21833
|
+
);
|
|
21834
|
+
server.registerTool(
|
|
21835
|
+
"update_change",
|
|
21836
|
+
{
|
|
21837
|
+
title: "Update Change",
|
|
21838
|
+
description: "Update provided fields of an existing Freshservice Change.",
|
|
21839
|
+
inputSchema: external_exports.object({
|
|
21840
|
+
change_id: external_exports.number().int().positive().describe("Freshservice Change ID"),
|
|
21841
|
+
...changeFields
|
|
21842
|
+
})
|
|
21843
|
+
},
|
|
21844
|
+
async (args) => {
|
|
21845
|
+
const change = await updateChange(config2, args.change_id, payloadFromArgs(args));
|
|
21846
|
+
return {
|
|
21847
|
+
content: [{
|
|
21848
|
+
type: "text",
|
|
21849
|
+
text: `Change #${args.change_id} updated.
|
|
21850
|
+
|
|
21851
|
+
${JSON.stringify(normalizeChange(change), null, 2)}`
|
|
21852
|
+
}]
|
|
21853
|
+
};
|
|
21854
|
+
}
|
|
21855
|
+
);
|
|
21856
|
+
server.registerTool(
|
|
21857
|
+
"associate_tickets_to_change",
|
|
21858
|
+
{
|
|
21859
|
+
title: "Associate Tickets to Change",
|
|
21860
|
+
description: "Associate existing tickets with a Change by updating each ticket using Freshservice's documented Change association object.",
|
|
21861
|
+
inputSchema: external_exports.object({
|
|
21862
|
+
change_id: external_exports.number().int().positive().describe(
|
|
21863
|
+
"Change display ID (Freshservice requires display_id in the association body)"
|
|
21864
|
+
),
|
|
21865
|
+
ticket_ids: external_exports.array(external_exports.number().int().positive()).min(1).max(100).describe(
|
|
21866
|
+
"Freshservice ticket IDs to associate"
|
|
21867
|
+
),
|
|
21868
|
+
association_type: external_exports.enum(["change_initiated_by_ticket", "change_initiating_ticket"]).optional().describe(
|
|
21869
|
+
"Relationship direction. Default: change_initiated_by_ticket (the ticket initiated the Change); use change_initiating_ticket when the Change initiated the ticket."
|
|
21870
|
+
)
|
|
21871
|
+
})
|
|
21872
|
+
},
|
|
21873
|
+
async ({ change_id, ticket_ids, association_type }) => {
|
|
21874
|
+
const tickets = await associateTicketsToChange(
|
|
21875
|
+
config2,
|
|
21876
|
+
change_id,
|
|
21877
|
+
ticket_ids,
|
|
21878
|
+
association_type
|
|
21879
|
+
);
|
|
21880
|
+
return {
|
|
21881
|
+
content: [{
|
|
21882
|
+
type: "text",
|
|
21883
|
+
text: JSON.stringify({
|
|
21884
|
+
change_id,
|
|
21885
|
+
association_type: association_type ?? "change_initiated_by_ticket",
|
|
21886
|
+
associated_ticket_ids: tickets.map((ticket) => ticket.id)
|
|
21887
|
+
}, null, 2)
|
|
21888
|
+
}]
|
|
21889
|
+
};
|
|
21890
|
+
}
|
|
21891
|
+
);
|
|
21892
|
+
server.registerTool(
|
|
21893
|
+
"add_change_note",
|
|
21894
|
+
{
|
|
21895
|
+
title: "Add Change Note",
|
|
21896
|
+
description: "Add a note to a Freshservice Change.",
|
|
21897
|
+
inputSchema: external_exports.object({
|
|
21898
|
+
change_id: external_exports.number().int().positive().describe("Freshservice Change ID"),
|
|
21899
|
+
body: external_exports.string().min(1).describe(`Note body. ${HTML_FIELD_HINT}`),
|
|
21900
|
+
notify_emails: external_exports.array(external_exports.string().email()).optional().describe("Email addresses to notify")
|
|
21901
|
+
})
|
|
21902
|
+
},
|
|
21903
|
+
async ({ change_id, body, notify_emails }) => {
|
|
21904
|
+
const note = await addChangeNote(config2, change_id, { body, notify_emails });
|
|
21905
|
+
return {
|
|
21906
|
+
content: [{
|
|
21907
|
+
type: "text",
|
|
21908
|
+
text: `Note added to Change #${change_id} (note id: ${note.id}).`
|
|
21909
|
+
}]
|
|
21910
|
+
};
|
|
21911
|
+
}
|
|
21912
|
+
);
|
|
21913
|
+
}
|
|
21914
|
+
|
|
21508
21915
|
// src/tools/agents.ts
|
|
21509
21916
|
init_esm();
|
|
21510
21917
|
init_freshservice_client();
|
|
@@ -21580,15 +21987,18 @@ async function main() {
|
|
|
21580
21987
|
const readonly2 = process.env.FRESHSERVICE_READONLY === "true";
|
|
21581
21988
|
const server = new McpServer({
|
|
21582
21989
|
name: "freshservice-mcp",
|
|
21583
|
-
version: "0.
|
|
21990
|
+
version: "0.2.0"
|
|
21584
21991
|
});
|
|
21585
21992
|
registerAgentTools(server, config2);
|
|
21586
21993
|
if (readonly2) {
|
|
21587
21994
|
const { registerReadOnlyTicketTools: registerReadOnlyTicketTools2 } = await Promise.resolve().then(() => (init_tickets_readonly(), tickets_readonly_exports));
|
|
21995
|
+
const { registerReadOnlyChangeTools: registerReadOnlyChangeTools2 } = await Promise.resolve().then(() => (init_changes_readonly(), changes_readonly_exports));
|
|
21588
21996
|
registerReadOnlyTicketTools2(server, config2);
|
|
21997
|
+
registerReadOnlyChangeTools2(server, config2);
|
|
21589
21998
|
console.error("Freshservice MCP Server running in read-only mode (stdio)");
|
|
21590
21999
|
} else {
|
|
21591
22000
|
registerTicketTools(server, config2);
|
|
22001
|
+
registerChangeTools(server, config2);
|
|
21592
22002
|
console.error("Freshservice MCP Server running on stdio");
|
|
21593
22003
|
}
|
|
21594
22004
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freshservice-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for Freshservice – manage tickets, agents and conversations from any MCP-compatible AI client",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for Freshservice – manage tickets, changes, agents and conversations from any MCP-compatible AI client",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
7
7
|
"freshservice",
|