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.
Files changed (3) hide show
  1. package/README.md +23 -2
  2. package/dist/index.js +415 -12
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
6
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](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, `create_ticket`, `update_ticket`, and `add_ticket_note` are not registered at all. Recommended for shared environments or autonomous agent setups against production instances.
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(label) {
4584
+ constructor(label3) {
4585
4585
  super();
4586
- this.label = 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(label) {
4594
+ constructor(label3) {
4595
4595
  super();
4596
- this.label = label;
4596
+ this.label = label3;
4597
4597
  this.names = {};
4598
4598
  }
4599
4599
  render({ _n }) {
4600
- const label = this.label ? ` ${this.label}` : "";
4601
- return `break${label};` + _n;
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(label) {
5014
- return this._leafNode(new Label(label));
5013
+ label(label3) {
5014
+ return this._leafNode(new Label(label3));
5015
5015
  }
5016
5016
  // `break` statement
5017
- break(label) {
5018
- return this._leafNode(new Break(label));
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.1.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.1.2",
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",