freshservice-mcp 0.1.2 → 0.2.1
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 +25 -2
- package/dist/index.js +433 -12
- 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,20 @@ 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
|
+
|
|
210
|
+
Because the association is applied as a ticket update, Freshservice validates the whole ticket — so on an instance where `description` is a mandatory field, a ticket with an empty description would otherwise be rejected with a 400. The tool handles this transparently: it retries once with a minimal placeholder description, and only for tickets that genuinely have none. Existing descriptions are never overwritten.
|
|
211
|
+
|
|
189
212
|
## Local Development
|
|
190
213
|
|
|
191
214
|
```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) {
|
|
@@ -11127,6 +11127,90 @@ 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) => associateOneTicket(config2, ticketId, changeDisplayId, associationType)
|
|
11184
|
+
)
|
|
11185
|
+
);
|
|
11186
|
+
}
|
|
11187
|
+
function isBlankDescriptionError(error2) {
|
|
11188
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
11189
|
+
return message.includes("400") && message.includes('"field":"description"') && message.toLowerCase().includes("blank");
|
|
11190
|
+
}
|
|
11191
|
+
async function associateOneTicket(config2, ticketId, changeDisplayId, associationType) {
|
|
11192
|
+
const association = { [associationType]: { display_id: changeDisplayId } };
|
|
11193
|
+
try {
|
|
11194
|
+
return await updateTicket(config2, ticketId, association);
|
|
11195
|
+
} catch (error2) {
|
|
11196
|
+
if (!isBlankDescriptionError(error2)) throw error2;
|
|
11197
|
+
const existing = await getTicket(config2, ticketId, "");
|
|
11198
|
+
const currentDescription = existing.description_text?.trim() || stripHtml(existing.description ?? "");
|
|
11199
|
+
if (currentDescription) throw error2;
|
|
11200
|
+
return updateTicket(config2, ticketId, {
|
|
11201
|
+
...association,
|
|
11202
|
+
description: BLANK_DESCRIPTION_PLACEHOLDER
|
|
11203
|
+
});
|
|
11204
|
+
}
|
|
11205
|
+
}
|
|
11206
|
+
async function addChangeNote(config2, changeId, payload) {
|
|
11207
|
+
const data = await fsPost(
|
|
11208
|
+
config2,
|
|
11209
|
+
`changes/${changeId}/notes`,
|
|
11210
|
+
payload
|
|
11211
|
+
);
|
|
11212
|
+
return data.note;
|
|
11213
|
+
}
|
|
11130
11214
|
async function getMe(config2) {
|
|
11131
11215
|
const data = await fsGet(config2, "agents/me");
|
|
11132
11216
|
return data.agent;
|
|
@@ -11139,10 +11223,11 @@ async function listAgents(config2, options = {}) {
|
|
|
11139
11223
|
const data = await fsGet(config2, "agents", params);
|
|
11140
11224
|
return data.agents ?? [];
|
|
11141
11225
|
}
|
|
11142
|
-
var DEFAULT_STATUS_MAP, PRIORITY_MAP;
|
|
11226
|
+
var BLANK_DESCRIPTION_PLACEHOLDER, DEFAULT_STATUS_MAP, PRIORITY_MAP, CHANGE_PRIORITY_MAP, CHANGE_STATUS_MAP, CHANGE_TYPE_MAP, CHANGE_IMPACT_MAP, CHANGE_RISK_MAP;
|
|
11143
11227
|
var init_freshservice_client = __esm({
|
|
11144
11228
|
"src/freshservice-client.ts"() {
|
|
11145
11229
|
"use strict";
|
|
11230
|
+
BLANK_DESCRIPTION_PLACEHOLDER = ".";
|
|
11146
11231
|
DEFAULT_STATUS_MAP = {
|
|
11147
11232
|
2: "Open",
|
|
11148
11233
|
3: "Pending",
|
|
@@ -11155,6 +11240,32 @@ var init_freshservice_client = __esm({
|
|
|
11155
11240
|
3: "High",
|
|
11156
11241
|
4: "Urgent"
|
|
11157
11242
|
};
|
|
11243
|
+
CHANGE_PRIORITY_MAP = PRIORITY_MAP;
|
|
11244
|
+
CHANGE_STATUS_MAP = {
|
|
11245
|
+
1: "Open",
|
|
11246
|
+
2: "Planning",
|
|
11247
|
+
3: "Awaiting Approval",
|
|
11248
|
+
4: "Pending Release",
|
|
11249
|
+
5: "Pending Review",
|
|
11250
|
+
6: "Closed"
|
|
11251
|
+
};
|
|
11252
|
+
CHANGE_TYPE_MAP = {
|
|
11253
|
+
1: "Minor",
|
|
11254
|
+
2: "Standard",
|
|
11255
|
+
3: "Major",
|
|
11256
|
+
4: "Emergency"
|
|
11257
|
+
};
|
|
11258
|
+
CHANGE_IMPACT_MAP = {
|
|
11259
|
+
1: "Low",
|
|
11260
|
+
2: "Medium",
|
|
11261
|
+
3: "High"
|
|
11262
|
+
};
|
|
11263
|
+
CHANGE_RISK_MAP = {
|
|
11264
|
+
1: "Low",
|
|
11265
|
+
2: "Medium",
|
|
11266
|
+
3: "High",
|
|
11267
|
+
4: "Very High"
|
|
11268
|
+
};
|
|
11158
11269
|
}
|
|
11159
11270
|
});
|
|
11160
11271
|
|
|
@@ -11279,6 +11390,94 @@ var init_tickets_readonly = __esm({
|
|
|
11279
11390
|
}
|
|
11280
11391
|
});
|
|
11281
11392
|
|
|
11393
|
+
// src/tools/changes-readonly.ts
|
|
11394
|
+
var changes_readonly_exports = {};
|
|
11395
|
+
__export(changes_readonly_exports, {
|
|
11396
|
+
registerReadOnlyChangeTools: () => registerReadOnlyChangeTools
|
|
11397
|
+
});
|
|
11398
|
+
function label2(value, values) {
|
|
11399
|
+
return values[value] ?? String(value);
|
|
11400
|
+
}
|
|
11401
|
+
function normalizeChange2(change) {
|
|
11402
|
+
return {
|
|
11403
|
+
id: change.id,
|
|
11404
|
+
subject: change.subject,
|
|
11405
|
+
status: label2(change.status, CHANGE_STATUS_MAP),
|
|
11406
|
+
priority: label2(change.priority, CHANGE_PRIORITY_MAP),
|
|
11407
|
+
impact: label2(change.impact, CHANGE_IMPACT_MAP),
|
|
11408
|
+
risk: label2(change.risk, CHANGE_RISK_MAP),
|
|
11409
|
+
change_type: label2(change.change_type, CHANGE_TYPE_MAP),
|
|
11410
|
+
requester_id: change.requester_id,
|
|
11411
|
+
agent_id: change.agent_id ?? null,
|
|
11412
|
+
group_id: change.group_id ?? null,
|
|
11413
|
+
planned_start_date: change.planned_start_date ?? null,
|
|
11414
|
+
planned_end_date: change.planned_end_date ?? null,
|
|
11415
|
+
created_at: change.created_at,
|
|
11416
|
+
updated_at: change.updated_at,
|
|
11417
|
+
url: `https://${process.env.FRESHSERVICE_DOMAIN}/helpdesk/changes/${change.id}`
|
|
11418
|
+
};
|
|
11419
|
+
}
|
|
11420
|
+
function registerReadOnlyChangeTools(server, config2) {
|
|
11421
|
+
server.registerTool(
|
|
11422
|
+
"list_changes",
|
|
11423
|
+
{
|
|
11424
|
+
title: "List Changes",
|
|
11425
|
+
description: "List Freshservice Changes with filtering, sorting and pagination.",
|
|
11426
|
+
inputSchema: external_exports.object({
|
|
11427
|
+
query: external_exports.string().max(512).optional(),
|
|
11428
|
+
view: external_exports.string().optional(),
|
|
11429
|
+
updated_since: external_exports.string().optional(),
|
|
11430
|
+
workspace_id: external_exports.number().int().nonnegative().optional(),
|
|
11431
|
+
order_by: external_exports.string().optional(),
|
|
11432
|
+
order_type: external_exports.enum(["asc", "desc"]).optional(),
|
|
11433
|
+
limit: external_exports.number().int().min(1).max(500).optional()
|
|
11434
|
+
}).refine(({ query, view }) => !(query && view), {
|
|
11435
|
+
message: "query and view cannot be used together"
|
|
11436
|
+
})
|
|
11437
|
+
},
|
|
11438
|
+
async (args) => {
|
|
11439
|
+
const changes = await listChanges(config2, args);
|
|
11440
|
+
if (changes.length === 0) {
|
|
11441
|
+
return { content: [{ type: "text", text: "No changes found." }] };
|
|
11442
|
+
}
|
|
11443
|
+
const normalized = changes.map(normalizeChange2);
|
|
11444
|
+
return {
|
|
11445
|
+
content: [{
|
|
11446
|
+
type: "text",
|
|
11447
|
+
text: JSON.stringify({ count: normalized.length, changes: normalized }, null, 2)
|
|
11448
|
+
}]
|
|
11449
|
+
};
|
|
11450
|
+
}
|
|
11451
|
+
);
|
|
11452
|
+
server.registerTool(
|
|
11453
|
+
"get_change",
|
|
11454
|
+
{
|
|
11455
|
+
title: "Get Change",
|
|
11456
|
+
description: "Retrieve full details of a Freshservice Change by ID.",
|
|
11457
|
+
inputSchema: external_exports.object({
|
|
11458
|
+
change_id: external_exports.number().int().positive(),
|
|
11459
|
+
include: external_exports.string().optional()
|
|
11460
|
+
})
|
|
11461
|
+
},
|
|
11462
|
+
async ({ change_id, include }) => {
|
|
11463
|
+
const change = await getChange(config2, change_id, include);
|
|
11464
|
+
const result = {
|
|
11465
|
+
...normalizeChange2(change),
|
|
11466
|
+
description: change.description_text?.trim() ?? stripHtml(change.description ?? ""),
|
|
11467
|
+
change_plan: change.planning_fields?.change_plan ? stripHtml(change.planning_fields.change_plan) : null
|
|
11468
|
+
};
|
|
11469
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
11470
|
+
}
|
|
11471
|
+
);
|
|
11472
|
+
}
|
|
11473
|
+
var init_changes_readonly = __esm({
|
|
11474
|
+
"src/tools/changes-readonly.ts"() {
|
|
11475
|
+
"use strict";
|
|
11476
|
+
init_esm();
|
|
11477
|
+
init_freshservice_client();
|
|
11478
|
+
}
|
|
11479
|
+
});
|
|
11480
|
+
|
|
11282
11481
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
|
|
11283
11482
|
init_esm();
|
|
11284
11483
|
|
|
@@ -21512,6 +21711,225 @@ ${JSON.stringify(result, null, 2)}`
|
|
|
21512
21711
|
);
|
|
21513
21712
|
}
|
|
21514
21713
|
|
|
21714
|
+
// src/tools/changes.ts
|
|
21715
|
+
init_esm();
|
|
21716
|
+
init_freshservice_client();
|
|
21717
|
+
function label(value, values) {
|
|
21718
|
+
return values[value] ?? String(value);
|
|
21719
|
+
}
|
|
21720
|
+
function normalizeChange(change) {
|
|
21721
|
+
return {
|
|
21722
|
+
id: change.id,
|
|
21723
|
+
subject: change.subject,
|
|
21724
|
+
status: label(change.status, CHANGE_STATUS_MAP),
|
|
21725
|
+
priority: label(change.priority, CHANGE_PRIORITY_MAP),
|
|
21726
|
+
impact: label(change.impact, CHANGE_IMPACT_MAP),
|
|
21727
|
+
risk: label(change.risk, CHANGE_RISK_MAP),
|
|
21728
|
+
change_type: label(change.change_type, CHANGE_TYPE_MAP),
|
|
21729
|
+
requester_id: change.requester_id,
|
|
21730
|
+
agent_id: change.agent_id ?? null,
|
|
21731
|
+
group_id: change.group_id ?? null,
|
|
21732
|
+
planned_start_date: change.planned_start_date ?? null,
|
|
21733
|
+
planned_end_date: change.planned_end_date ?? null,
|
|
21734
|
+
created_at: change.created_at,
|
|
21735
|
+
updated_at: change.updated_at,
|
|
21736
|
+
url: `https://${process.env.FRESHSERVICE_DOMAIN}/helpdesk/changes/${change.id}`
|
|
21737
|
+
};
|
|
21738
|
+
}
|
|
21739
|
+
var listChangesSchema = external_exports.object({
|
|
21740
|
+
query: external_exports.string().max(512).optional().describe('Freshservice query, e.g. "priority:4 OR priority:3"'),
|
|
21741
|
+
view: external_exports.string().optional().describe("Default view name or custom Change view ID; cannot be combined with query"),
|
|
21742
|
+
updated_since: external_exports.string().optional().describe("Return Changes updated since this UTC date or timestamp"),
|
|
21743
|
+
workspace_id: external_exports.number().int().nonnegative().optional().describe("Workspace ID; 0 requests Changes across all accessible workspaces"),
|
|
21744
|
+
order_by: external_exports.string().optional().describe("Field to sort by, e.g. priority or updated_at"),
|
|
21745
|
+
order_type: external_exports.enum(["asc", "desc"]).optional().describe("Sort direction (default: desc)"),
|
|
21746
|
+
limit: external_exports.number().int().min(1).max(500).optional().describe("Maximum results (default: 100)")
|
|
21747
|
+
}).refine(({ query, view }) => !(query && view), {
|
|
21748
|
+
message: "query and view cannot be used together"
|
|
21749
|
+
});
|
|
21750
|
+
var changeFields = {
|
|
21751
|
+
subject: external_exports.string().min(1).optional().describe("Change subject"),
|
|
21752
|
+
description: external_exports.string().min(1).optional().describe(`Change description. ${HTML_FIELD_HINT}`),
|
|
21753
|
+
requester_id: external_exports.number().int().positive().optional().describe("Initiating requester ID"),
|
|
21754
|
+
priority: external_exports.enum(["1", "2", "3", "4"]).optional().describe("1=Low, 2=Medium, 3=High, 4=Urgent"),
|
|
21755
|
+
status: external_exports.enum(["1", "2", "3", "4", "5", "6"]).optional().describe(
|
|
21756
|
+
"1=Open, 2=Planning, 3=Awaiting Approval, 4=Pending Release, 5=Pending Review, 6=Closed"
|
|
21757
|
+
),
|
|
21758
|
+
impact: external_exports.enum(["1", "2", "3"]).optional().describe("1=Low, 2=Medium, 3=High"),
|
|
21759
|
+
risk: external_exports.enum(["1", "2", "3", "4"]).optional().describe("1=Low, 2=Medium, 3=High, 4=Very High"),
|
|
21760
|
+
change_type: external_exports.enum(["1", "2", "3", "4"]).optional().describe(
|
|
21761
|
+
"1=Minor, 2=Standard, 3=Major, 4=Emergency"
|
|
21762
|
+
),
|
|
21763
|
+
planned_start_date: external_exports.string().optional().describe("Planned start as an ISO 8601 UTC timestamp"),
|
|
21764
|
+
planned_end_date: external_exports.string().optional().describe("Planned end as an ISO 8601 UTC timestamp"),
|
|
21765
|
+
change_plan: external_exports.string().optional().describe(`Rollout plan. ${HTML_FIELD_HINT}`),
|
|
21766
|
+
group_id: external_exports.number().int().positive().optional().describe("Assigned agent group ID"),
|
|
21767
|
+
agent_id: external_exports.number().int().positive().optional().describe("Assigned agent ID")
|
|
21768
|
+
};
|
|
21769
|
+
function payloadFromArgs(args) {
|
|
21770
|
+
const payload = {};
|
|
21771
|
+
for (const [key, value] of Object.entries(args)) {
|
|
21772
|
+
if (value === void 0 || key === "change_id") continue;
|
|
21773
|
+
payload[key] = ["priority", "status", "impact", "risk", "change_type"].includes(key) ? Number(value) : value;
|
|
21774
|
+
}
|
|
21775
|
+
return payload;
|
|
21776
|
+
}
|
|
21777
|
+
function registerChangeTools(server, config2) {
|
|
21778
|
+
server.registerTool(
|
|
21779
|
+
"list_changes",
|
|
21780
|
+
{
|
|
21781
|
+
title: "List Changes",
|
|
21782
|
+
description: "List Freshservice Changes with filtering, sorting and pagination.",
|
|
21783
|
+
inputSchema: listChangesSchema
|
|
21784
|
+
},
|
|
21785
|
+
async (args) => {
|
|
21786
|
+
const changes = await listChanges(config2, args);
|
|
21787
|
+
if (changes.length === 0) {
|
|
21788
|
+
return { content: [{ type: "text", text: "No changes found." }] };
|
|
21789
|
+
}
|
|
21790
|
+
const normalized = changes.map(normalizeChange);
|
|
21791
|
+
return {
|
|
21792
|
+
content: [{
|
|
21793
|
+
type: "text",
|
|
21794
|
+
text: JSON.stringify({ count: normalized.length, changes: normalized }, null, 2)
|
|
21795
|
+
}]
|
|
21796
|
+
};
|
|
21797
|
+
}
|
|
21798
|
+
);
|
|
21799
|
+
server.registerTool(
|
|
21800
|
+
"get_change",
|
|
21801
|
+
{
|
|
21802
|
+
title: "Get Change",
|
|
21803
|
+
description: "Retrieve full details of a Freshservice Change by ID.",
|
|
21804
|
+
inputSchema: external_exports.object({
|
|
21805
|
+
change_id: external_exports.number().int().positive().describe("Freshservice Change ID"),
|
|
21806
|
+
include: external_exports.string().optional().describe("Extra details to embed, e.g. stats")
|
|
21807
|
+
})
|
|
21808
|
+
},
|
|
21809
|
+
async ({ change_id, include }) => {
|
|
21810
|
+
const change = await getChange(config2, change_id, include);
|
|
21811
|
+
const result = {
|
|
21812
|
+
...normalizeChange(change),
|
|
21813
|
+
description: change.description_text?.trim() ?? stripHtml(change.description ?? ""),
|
|
21814
|
+
change_plan: change.planning_fields?.change_plan ? stripHtml(change.planning_fields.change_plan) : null
|
|
21815
|
+
};
|
|
21816
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
21817
|
+
}
|
|
21818
|
+
);
|
|
21819
|
+
server.registerTool(
|
|
21820
|
+
"create_change",
|
|
21821
|
+
{
|
|
21822
|
+
title: "Create Change",
|
|
21823
|
+
description: "Create a Freshservice Change with all standard mandatory fields.",
|
|
21824
|
+
inputSchema: external_exports.object({
|
|
21825
|
+
subject: changeFields.subject.unwrap(),
|
|
21826
|
+
description: changeFields.description.unwrap(),
|
|
21827
|
+
requester_id: changeFields.requester_id.unwrap(),
|
|
21828
|
+
priority: changeFields.priority.unwrap(),
|
|
21829
|
+
status: changeFields.status.unwrap(),
|
|
21830
|
+
impact: changeFields.impact.unwrap(),
|
|
21831
|
+
risk: changeFields.risk.unwrap(),
|
|
21832
|
+
change_type: changeFields.change_type.unwrap(),
|
|
21833
|
+
planned_start_date: changeFields.planned_start_date.unwrap(),
|
|
21834
|
+
planned_end_date: changeFields.planned_end_date.unwrap(),
|
|
21835
|
+
change_plan: changeFields.change_plan,
|
|
21836
|
+
group_id: changeFields.group_id,
|
|
21837
|
+
agent_id: changeFields.agent_id
|
|
21838
|
+
})
|
|
21839
|
+
},
|
|
21840
|
+
async (args) => {
|
|
21841
|
+
const change = await createChange(config2, payloadFromArgs(args));
|
|
21842
|
+
return {
|
|
21843
|
+
content: [{
|
|
21844
|
+
type: "text",
|
|
21845
|
+
text: `Change #${change.id} created.
|
|
21846
|
+
|
|
21847
|
+
${JSON.stringify(normalizeChange(change), null, 2)}`
|
|
21848
|
+
}]
|
|
21849
|
+
};
|
|
21850
|
+
}
|
|
21851
|
+
);
|
|
21852
|
+
server.registerTool(
|
|
21853
|
+
"update_change",
|
|
21854
|
+
{
|
|
21855
|
+
title: "Update Change",
|
|
21856
|
+
description: "Update provided fields of an existing Freshservice Change.",
|
|
21857
|
+
inputSchema: external_exports.object({
|
|
21858
|
+
change_id: external_exports.number().int().positive().describe("Freshservice Change ID"),
|
|
21859
|
+
...changeFields
|
|
21860
|
+
})
|
|
21861
|
+
},
|
|
21862
|
+
async (args) => {
|
|
21863
|
+
const change = await updateChange(config2, args.change_id, payloadFromArgs(args));
|
|
21864
|
+
return {
|
|
21865
|
+
content: [{
|
|
21866
|
+
type: "text",
|
|
21867
|
+
text: `Change #${args.change_id} updated.
|
|
21868
|
+
|
|
21869
|
+
${JSON.stringify(normalizeChange(change), null, 2)}`
|
|
21870
|
+
}]
|
|
21871
|
+
};
|
|
21872
|
+
}
|
|
21873
|
+
);
|
|
21874
|
+
server.registerTool(
|
|
21875
|
+
"associate_tickets_to_change",
|
|
21876
|
+
{
|
|
21877
|
+
title: "Associate Tickets to Change",
|
|
21878
|
+
description: "Associate existing tickets with a Change by updating each ticket using Freshservice's documented Change association object. A ticket whose description is empty on an instance where description is mandatory is retried once with a minimal placeholder description so the association still lands; existing descriptions are never overwritten.",
|
|
21879
|
+
inputSchema: external_exports.object({
|
|
21880
|
+
change_id: external_exports.number().int().positive().describe(
|
|
21881
|
+
"Change display ID (Freshservice requires display_id in the association body)"
|
|
21882
|
+
),
|
|
21883
|
+
ticket_ids: external_exports.array(external_exports.number().int().positive()).min(1).max(100).describe(
|
|
21884
|
+
"Freshservice ticket IDs to associate"
|
|
21885
|
+
),
|
|
21886
|
+
association_type: external_exports.enum(["change_initiated_by_ticket", "change_initiating_ticket"]).optional().describe(
|
|
21887
|
+
"Relationship direction. Default: change_initiated_by_ticket (the ticket initiated the Change); use change_initiating_ticket when the Change initiated the ticket."
|
|
21888
|
+
)
|
|
21889
|
+
})
|
|
21890
|
+
},
|
|
21891
|
+
async ({ change_id, ticket_ids, association_type }) => {
|
|
21892
|
+
const tickets = await associateTicketsToChange(
|
|
21893
|
+
config2,
|
|
21894
|
+
change_id,
|
|
21895
|
+
ticket_ids,
|
|
21896
|
+
association_type
|
|
21897
|
+
);
|
|
21898
|
+
return {
|
|
21899
|
+
content: [{
|
|
21900
|
+
type: "text",
|
|
21901
|
+
text: JSON.stringify({
|
|
21902
|
+
change_id,
|
|
21903
|
+
association_type: association_type ?? "change_initiated_by_ticket",
|
|
21904
|
+
associated_ticket_ids: tickets.map((ticket) => ticket.id)
|
|
21905
|
+
}, null, 2)
|
|
21906
|
+
}]
|
|
21907
|
+
};
|
|
21908
|
+
}
|
|
21909
|
+
);
|
|
21910
|
+
server.registerTool(
|
|
21911
|
+
"add_change_note",
|
|
21912
|
+
{
|
|
21913
|
+
title: "Add Change Note",
|
|
21914
|
+
description: "Add a note to a Freshservice Change.",
|
|
21915
|
+
inputSchema: external_exports.object({
|
|
21916
|
+
change_id: external_exports.number().int().positive().describe("Freshservice Change ID"),
|
|
21917
|
+
body: external_exports.string().min(1).describe(`Note body. ${HTML_FIELD_HINT}`),
|
|
21918
|
+
notify_emails: external_exports.array(external_exports.string().email()).optional().describe("Email addresses to notify")
|
|
21919
|
+
})
|
|
21920
|
+
},
|
|
21921
|
+
async ({ change_id, body, notify_emails }) => {
|
|
21922
|
+
const note = await addChangeNote(config2, change_id, { body, notify_emails });
|
|
21923
|
+
return {
|
|
21924
|
+
content: [{
|
|
21925
|
+
type: "text",
|
|
21926
|
+
text: `Note added to Change #${change_id} (note id: ${note.id}).`
|
|
21927
|
+
}]
|
|
21928
|
+
};
|
|
21929
|
+
}
|
|
21930
|
+
);
|
|
21931
|
+
}
|
|
21932
|
+
|
|
21515
21933
|
// src/tools/agents.ts
|
|
21516
21934
|
init_esm();
|
|
21517
21935
|
init_freshservice_client();
|
|
@@ -21587,15 +22005,18 @@ async function main() {
|
|
|
21587
22005
|
const readonly2 = process.env.FRESHSERVICE_READONLY === "true";
|
|
21588
22006
|
const server = new McpServer({
|
|
21589
22007
|
name: "freshservice-mcp",
|
|
21590
|
-
version: "0.
|
|
22008
|
+
version: "0.2.0"
|
|
21591
22009
|
});
|
|
21592
22010
|
registerAgentTools(server, config2);
|
|
21593
22011
|
if (readonly2) {
|
|
21594
22012
|
const { registerReadOnlyTicketTools: registerReadOnlyTicketTools2 } = await Promise.resolve().then(() => (init_tickets_readonly(), tickets_readonly_exports));
|
|
22013
|
+
const { registerReadOnlyChangeTools: registerReadOnlyChangeTools2 } = await Promise.resolve().then(() => (init_changes_readonly(), changes_readonly_exports));
|
|
21595
22014
|
registerReadOnlyTicketTools2(server, config2);
|
|
22015
|
+
registerReadOnlyChangeTools2(server, config2);
|
|
21596
22016
|
console.error("Freshservice MCP Server running in read-only mode (stdio)");
|
|
21597
22017
|
} else {
|
|
21598
22018
|
registerTicketTools(server, config2);
|
|
22019
|
+
registerChangeTools(server, config2);
|
|
21599
22020
|
console.error("Freshservice MCP Server running on stdio");
|
|
21600
22021
|
}
|
|
21601
22022
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freshservice-mcp",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "MCP server for Freshservice – manage tickets, agents and conversations from any MCP-compatible AI client",
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|