freshservice-mcp 0.1.2 → 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 +415 -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,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) {
|
|
@@ -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
|
|
|
@@ -21512,6 +21693,225 @@ ${JSON.stringify(result, null, 2)}`
|
|
|
21512
21693
|
);
|
|
21513
21694
|
}
|
|
21514
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
|
+
|
|
21515
21915
|
// src/tools/agents.ts
|
|
21516
21916
|
init_esm();
|
|
21517
21917
|
init_freshservice_client();
|
|
@@ -21587,15 +21987,18 @@ async function main() {
|
|
|
21587
21987
|
const readonly2 = process.env.FRESHSERVICE_READONLY === "true";
|
|
21588
21988
|
const server = new McpServer({
|
|
21589
21989
|
name: "freshservice-mcp",
|
|
21590
|
-
version: "0.
|
|
21990
|
+
version: "0.2.0"
|
|
21591
21991
|
});
|
|
21592
21992
|
registerAgentTools(server, config2);
|
|
21593
21993
|
if (readonly2) {
|
|
21594
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));
|
|
21595
21996
|
registerReadOnlyTicketTools2(server, config2);
|
|
21997
|
+
registerReadOnlyChangeTools2(server, config2);
|
|
21596
21998
|
console.error("Freshservice MCP Server running in read-only mode (stdio)");
|
|
21597
21999
|
} else {
|
|
21598
22000
|
registerTicketTools(server, config2);
|
|
22001
|
+
registerChangeTools(server, config2);
|
|
21599
22002
|
console.error("Freshservice MCP Server running on stdio");
|
|
21600
22003
|
}
|
|
21601
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",
|