@softeria/ms-365-mcp-server 0.47.0 → 0.47.2

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/dist/auth.js CHANGED
@@ -48,6 +48,30 @@ function ensureParentDir(filePath) {
48
48
  const dir = path.dirname(filePath);
49
49
  fs.mkdirSync(dir, { recursive: true, mode: 448 });
50
50
  }
51
+ function wrapCache(data) {
52
+ return JSON.stringify({ _cacheEnvelope: true, data, savedAt: Date.now() });
53
+ }
54
+ function unwrapCache(raw) {
55
+ try {
56
+ const parsed = JSON.parse(raw);
57
+ if (parsed._cacheEnvelope && typeof parsed.data === "string") {
58
+ return { data: parsed.data, savedAt: parsed.savedAt };
59
+ }
60
+ } catch {
61
+ }
62
+ return { data: raw };
63
+ }
64
+ function pickNewest(keytarRaw, fileRaw) {
65
+ if (!keytarRaw && !fileRaw) return void 0;
66
+ if (keytarRaw && !fileRaw) return unwrapCache(keytarRaw).data;
67
+ if (!keytarRaw && fileRaw) return unwrapCache(fileRaw).data;
68
+ const kt = unwrapCache(keytarRaw);
69
+ const file = unwrapCache(fileRaw);
70
+ if (kt.savedAt === void 0 && file.savedAt === void 0) return kt.data;
71
+ if (kt.savedAt !== void 0 && file.savedAt === void 0) return kt.data;
72
+ if (kt.savedAt === void 0 && file.savedAt !== void 0) return file.data;
73
+ return kt.savedAt >= file.savedAt ? kt.data : file.data;
74
+ }
51
75
  function createMsalConfig(secrets) {
52
76
  const cloudEndpoints = getCloudEndpoints(secrets.cloudType);
53
77
  return {
@@ -126,24 +150,21 @@ class AuthManager {
126
150
  }
127
151
  async loadTokenCache() {
128
152
  try {
129
- let cacheData;
153
+ let keytarRaw;
130
154
  try {
131
155
  const kt = await getKeytar();
132
156
  if (kt) {
133
- const cachedData = await kt.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT);
134
- if (cachedData) {
135
- cacheData = cachedData;
136
- }
157
+ keytarRaw = await kt.getPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT) ?? void 0;
137
158
  }
138
159
  } catch (keytarError) {
139
- logger.warn(
140
- `Keychain access failed, falling back to file storage: ${keytarError.message}`
141
- );
160
+ logger.warn(`Keychain access failed: ${keytarError.message}`);
142
161
  }
162
+ let fileRaw;
143
163
  const cachePath = getTokenCachePath();
144
- if (!cacheData && existsSync(cachePath)) {
145
- cacheData = readFileSync(cachePath, "utf8");
164
+ if (existsSync(cachePath)) {
165
+ fileRaw = readFileSync(cachePath, "utf8");
146
166
  }
167
+ const cacheData = pickNewest(keytarRaw, fileRaw);
147
168
  if (cacheData) {
148
169
  this.msalApp.getTokenCache().deserialize(cacheData);
149
170
  }
@@ -154,24 +175,23 @@ class AuthManager {
154
175
  }
155
176
  async loadSelectedAccount() {
156
177
  try {
157
- let selectedAccountData;
178
+ let keytarRaw;
158
179
  try {
159
180
  const kt = await getKeytar();
160
181
  if (kt) {
161
- const cachedData = await kt.getPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY);
162
- if (cachedData) {
163
- selectedAccountData = cachedData;
164
- }
182
+ keytarRaw = await kt.getPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY) ?? void 0;
165
183
  }
166
184
  } catch (keytarError) {
167
185
  logger.warn(
168
- `Keychain access failed for selected account, falling back to file storage: ${keytarError.message}`
186
+ `Keychain access failed for selected account: ${keytarError.message}`
169
187
  );
170
188
  }
189
+ let fileRaw;
171
190
  const accountPath = getSelectedAccountPath();
172
- if (!selectedAccountData && existsSync(accountPath)) {
173
- selectedAccountData = readFileSync(accountPath, "utf8");
191
+ if (existsSync(accountPath)) {
192
+ fileRaw = readFileSync(accountPath, "utf8");
174
193
  }
194
+ const selectedAccountData = pickNewest(keytarRaw, fileRaw);
175
195
  if (selectedAccountData) {
176
196
  const parsed = JSON.parse(selectedAccountData);
177
197
  this.selectedAccountId = parsed.accountId;
@@ -183,15 +203,15 @@ class AuthManager {
183
203
  }
184
204
  async saveTokenCache() {
185
205
  try {
186
- const cacheData = this.msalApp.getTokenCache().serialize();
206
+ const stamped = wrapCache(this.msalApp.getTokenCache().serialize());
187
207
  try {
188
208
  const kt = await getKeytar();
189
209
  if (kt) {
190
- await kt.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, cacheData);
210
+ await kt.setPassword(SERVICE_NAME, TOKEN_CACHE_ACCOUNT, stamped);
191
211
  } else {
192
212
  const cachePath = getTokenCachePath();
193
213
  ensureParentDir(cachePath);
194
- fs.writeFileSync(cachePath, cacheData, { mode: 384 });
214
+ fs.writeFileSync(cachePath, stamped, { mode: 384 });
195
215
  }
196
216
  } catch (keytarError) {
197
217
  logger.warn(
@@ -199,7 +219,7 @@ class AuthManager {
199
219
  );
200
220
  const cachePath = getTokenCachePath();
201
221
  ensureParentDir(cachePath);
202
- fs.writeFileSync(cachePath, cacheData, { mode: 384 });
222
+ fs.writeFileSync(cachePath, stamped, { mode: 384 });
203
223
  }
204
224
  } catch (error) {
205
225
  logger.error(`Error saving token cache: ${error.message}`);
@@ -207,15 +227,15 @@ class AuthManager {
207
227
  }
208
228
  async saveSelectedAccount() {
209
229
  try {
210
- const selectedAccountData = JSON.stringify({ accountId: this.selectedAccountId });
230
+ const stamped = wrapCache(JSON.stringify({ accountId: this.selectedAccountId }));
211
231
  try {
212
232
  const kt = await getKeytar();
213
233
  if (kt) {
214
- await kt.setPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY, selectedAccountData);
234
+ await kt.setPassword(SERVICE_NAME, SELECTED_ACCOUNT_KEY, stamped);
215
235
  } else {
216
236
  const accountPath = getSelectedAccountPath();
217
237
  ensureParentDir(accountPath);
218
- fs.writeFileSync(accountPath, selectedAccountData, { mode: 384 });
238
+ fs.writeFileSync(accountPath, stamped, { mode: 384 });
219
239
  }
220
240
  } catch (keytarError) {
221
241
  logger.warn(
@@ -223,7 +243,7 @@ class AuthManager {
223
243
  );
224
244
  const accountPath = getSelectedAccountPath();
225
245
  ensureParentDir(accountPath);
226
- fs.writeFileSync(accountPath, selectedAccountData, { mode: 384 });
246
+ fs.writeFileSync(accountPath, stamped, { mode: 384 });
227
247
  }
228
248
  } catch (error) {
229
249
  logger.error(`Error saving selected account: ${error.message}`);
@@ -524,5 +544,8 @@ export {
524
544
  buildScopesFromEndpoints,
525
545
  auth_default as default,
526
546
  getSelectedAccountPath,
527
- getTokenCachePath
547
+ getTokenCachePath,
548
+ pickNewest,
549
+ unwrapCache,
550
+ wrapCache
528
551
  };
@@ -658,6 +658,55 @@
658
658
  "toolName": "list-team-members",
659
659
  "workScopes": ["TeamMember.Read.All"]
660
660
  },
661
+ {
662
+ "pathPattern": "/teams/{team-id}/tags",
663
+ "method": "get",
664
+ "toolName": "list-team-tags",
665
+ "workScopes": ["TeamworkTag.Read"],
666
+ "llmTip": "List all tags in a team. Returns tag id, displayName, memberCount, and tagType (standard or scheduled). Use list-joined-teams to find the team ID first."
667
+ },
668
+ {
669
+ "pathPattern": "/teams/{team-id}/tags",
670
+ "method": "post",
671
+ "toolName": "create-team-tag",
672
+ "workScopes": ["TeamworkTag.ReadWrite"],
673
+ "llmTip": "Create a new tag in a team. Body requires displayName (max 40 chars) and an optional members array (max 25 at creation, each with a userId). Use list-team-members to find user IDs. Add more members individually with add-team-tag-member after creation."
674
+ },
675
+ {
676
+ "pathPattern": "/teams/{team-id}/tags/{teamworkTag-id}",
677
+ "method": "get",
678
+ "toolName": "get-team-tag",
679
+ "workScopes": ["TeamworkTag.Read"],
680
+ "llmTip": "Get details of a specific tag including displayName, memberCount, and tagType. Use list-team-tags to find the tag ID."
681
+ },
682
+ {
683
+ "pathPattern": "/teams/{team-id}/tags/{teamworkTag-id}",
684
+ "method": "patch",
685
+ "toolName": "update-team-tag",
686
+ "workScopes": ["TeamworkTag.ReadWrite"],
687
+ "llmTip": "Update a tag's displayName (max 40 chars). Use list-team-tags to find the tag ID."
688
+ },
689
+ {
690
+ "pathPattern": "/teams/{team-id}/tags/{teamworkTag-id}",
691
+ "method": "delete",
692
+ "toolName": "delete-team-tag",
693
+ "workScopes": ["TeamworkTag.ReadWrite"],
694
+ "llmTip": "Permanently delete a tag from a team. This action is irreversible. Use list-team-tags to find the tag ID."
695
+ },
696
+ {
697
+ "pathPattern": "/teams/{team-id}/tags/{teamworkTag-id}/members",
698
+ "method": "get",
699
+ "toolName": "list-team-tag-members",
700
+ "workScopes": ["TeamworkTag.Read"],
701
+ "llmTip": "List all members of a specific tag. Returns member id, displayName, tenantId, and userId. Use list-team-tags to find the tag ID."
702
+ },
703
+ {
704
+ "pathPattern": "/teams/{team-id}/tags/{teamworkTag-id}/members/{teamworkTagMember-id}",
705
+ "method": "delete",
706
+ "toolName": "remove-team-tag-member",
707
+ "workScopes": ["TeamworkTag.ReadWrite"],
708
+ "llmTip": "Remove a member from a tag. Use list-team-tag-members to find the member ID."
709
+ },
661
710
  {
662
711
  "pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/replies",
663
712
  "method": "get",
@@ -3559,6 +3559,34 @@ const microsoft_graph_conversationMemberCollectionResponse = z.object({
3559
3559
  "@odata.nextLink": z.string().nullable(),
3560
3560
  value: z.array(microsoft_graph_conversationMember)
3561
3561
  }).partial().passthrough();
3562
+ const microsoft_graph_teamworkTagType = z.enum(["standard", "unknownFutureValue"]);
3563
+ const microsoft_graph_teamworkTagMember = z.object({
3564
+ id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
3565
+ displayName: z.string().describe("The member's display name.").nullish(),
3566
+ tenantId: z.string().describe("The ID of the tenant that the tag member is a part of.").nullish(),
3567
+ userId: z.string().describe("The user ID of the member.").nullish()
3568
+ }).passthrough();
3569
+ const microsoft_graph_teamworkTag = z.object({
3570
+ id: z.string().describe("The unique identifier for an entity. Read-only.").optional(),
3571
+ description: z.string().describe(
3572
+ "The description of the tag as it appears to the user in Microsoft Teams. A teamworkTag can't have more than 200 teamworkTagMembers."
3573
+ ).nullish(),
3574
+ displayName: z.string().describe("The name of the tag as it appears to the user in Microsoft Teams.").nullish(),
3575
+ memberCount: z.number().gte(-2147483648).lte(2147483647).describe("The number of users assigned to the tag.").nullish(),
3576
+ tagType: microsoft_graph_teamworkTagType.optional(),
3577
+ teamId: z.string().describe("ID of the team in which the tag is defined.").nullish(),
3578
+ members: z.array(microsoft_graph_teamworkTagMember).describe("Users assigned to the tag.").optional()
3579
+ }).passthrough();
3580
+ const microsoft_graph_teamworkTagCollectionResponse = z.object({
3581
+ "@odata.count": z.number().int().nullable(),
3582
+ "@odata.nextLink": z.string().nullable(),
3583
+ value: z.array(microsoft_graph_teamworkTag)
3584
+ }).partial().passthrough();
3585
+ const microsoft_graph_teamworkTagMemberCollectionResponse = z.object({
3586
+ "@odata.count": z.number().int().nullable(),
3587
+ "@odata.nextLink": z.string().nullable(),
3588
+ value: z.array(microsoft_graph_teamworkTagMember)
3589
+ }).partial().passthrough();
3562
3590
  const microsoft_graph_userCollectionResponse = z.object({
3563
3591
  "@odata.count": z.number().int().nullable(),
3564
3592
  "@odata.nextLink": z.string().nullable(),
@@ -3919,6 +3947,11 @@ const schemas = {
3919
3947
  BaseDeltaFunctionResponse,
3920
3948
  microsoft_graph_channelCollectionResponse,
3921
3949
  microsoft_graph_conversationMemberCollectionResponse,
3950
+ microsoft_graph_teamworkTagType,
3951
+ microsoft_graph_teamworkTagMember,
3952
+ microsoft_graph_teamworkTag,
3953
+ microsoft_graph_teamworkTagCollectionResponse,
3954
+ microsoft_graph_teamworkTagMemberCollectionResponse,
3922
3955
  microsoft_graph_userCollectionResponse
3923
3956
  };
3924
3957
  const endpoints = makeApi([
@@ -7924,6 +7957,188 @@ To monitor future changes, call the delta API by using the @odata.deltaLink in t
7924
7957
  ],
7925
7958
  response: z.void()
7926
7959
  },
7960
+ {
7961
+ method: "get",
7962
+ path: "/teams/:teamId/tags",
7963
+ alias: "list-team-tags",
7964
+ description: `Get a list of the tag objects and their properties.`,
7965
+ requestFormat: "json",
7966
+ parameters: [
7967
+ {
7968
+ name: "$top",
7969
+ type: "Query",
7970
+ schema: z.number().int().gte(0).describe("Show only the first n items").optional()
7971
+ },
7972
+ {
7973
+ name: "$skip",
7974
+ type: "Query",
7975
+ schema: z.number().int().gte(0).describe("Skip the first n items").optional()
7976
+ },
7977
+ {
7978
+ name: "$search",
7979
+ type: "Query",
7980
+ schema: z.string().describe("Search items by search phrases").optional()
7981
+ },
7982
+ {
7983
+ name: "$filter",
7984
+ type: "Query",
7985
+ schema: z.string().describe("Filter items by property values").optional()
7986
+ },
7987
+ {
7988
+ name: "$count",
7989
+ type: "Query",
7990
+ schema: z.boolean().describe("Include count of items").optional()
7991
+ },
7992
+ {
7993
+ name: "$orderby",
7994
+ type: "Query",
7995
+ schema: z.array(z.string()).describe("Order items by property values").optional()
7996
+ },
7997
+ {
7998
+ name: "$select",
7999
+ type: "Query",
8000
+ schema: z.array(z.string()).describe("Select properties to be returned").optional()
8001
+ },
8002
+ {
8003
+ name: "$expand",
8004
+ type: "Query",
8005
+ schema: z.array(z.string()).describe("Expand related entities").optional()
8006
+ }
8007
+ ],
8008
+ response: z.void()
8009
+ },
8010
+ {
8011
+ method: "post",
8012
+ path: "/teams/:teamId/tags",
8013
+ alias: "create-team-tag",
8014
+ description: `Create a standard tag for members in a team.`,
8015
+ requestFormat: "json",
8016
+ parameters: [
8017
+ {
8018
+ name: "body",
8019
+ description: `New navigation property`,
8020
+ type: "Body",
8021
+ schema: microsoft_graph_teamworkTag
8022
+ }
8023
+ ],
8024
+ response: z.void()
8025
+ },
8026
+ {
8027
+ method: "get",
8028
+ path: "/teams/:teamId/tags/:teamworkTagId",
8029
+ alias: "get-team-tag",
8030
+ description: `Read the properties and relationships of a tag object.`,
8031
+ requestFormat: "json",
8032
+ parameters: [
8033
+ {
8034
+ name: "$select",
8035
+ type: "Query",
8036
+ schema: z.array(z.string()).describe("Select properties to be returned").optional()
8037
+ },
8038
+ {
8039
+ name: "$expand",
8040
+ type: "Query",
8041
+ schema: z.array(z.string()).describe("Expand related entities").optional()
8042
+ }
8043
+ ],
8044
+ response: z.void()
8045
+ },
8046
+ {
8047
+ method: "patch",
8048
+ path: "/teams/:teamId/tags/:teamworkTagId",
8049
+ alias: "update-team-tag",
8050
+ description: `Update the properties of a tag object.`,
8051
+ requestFormat: "json",
8052
+ parameters: [
8053
+ {
8054
+ name: "body",
8055
+ description: `New navigation property values`,
8056
+ type: "Body",
8057
+ schema: microsoft_graph_teamworkTag
8058
+ }
8059
+ ],
8060
+ response: z.void()
8061
+ },
8062
+ {
8063
+ method: "delete",
8064
+ path: "/teams/:teamId/tags/:teamworkTagId",
8065
+ alias: "delete-team-tag",
8066
+ description: `Delete a tag object permanently.`,
8067
+ requestFormat: "json",
8068
+ parameters: [
8069
+ {
8070
+ name: "If-Match",
8071
+ type: "Header",
8072
+ schema: z.string().describe("ETag").optional()
8073
+ }
8074
+ ],
8075
+ response: z.void()
8076
+ },
8077
+ {
8078
+ method: "get",
8079
+ path: "/teams/:teamId/tags/:teamworkTagId/members",
8080
+ alias: "list-team-tag-members",
8081
+ description: `Get a list of the members of a standard tag in a team and their properties.`,
8082
+ requestFormat: "json",
8083
+ parameters: [
8084
+ {
8085
+ name: "$top",
8086
+ type: "Query",
8087
+ schema: z.number().int().gte(0).describe("Show only the first n items").optional()
8088
+ },
8089
+ {
8090
+ name: "$skip",
8091
+ type: "Query",
8092
+ schema: z.number().int().gte(0).describe("Skip the first n items").optional()
8093
+ },
8094
+ {
8095
+ name: "$search",
8096
+ type: "Query",
8097
+ schema: z.string().describe("Search items by search phrases").optional()
8098
+ },
8099
+ {
8100
+ name: "$filter",
8101
+ type: "Query",
8102
+ schema: z.string().describe("Filter items by property values").optional()
8103
+ },
8104
+ {
8105
+ name: "$count",
8106
+ type: "Query",
8107
+ schema: z.boolean().describe("Include count of items").optional()
8108
+ },
8109
+ {
8110
+ name: "$orderby",
8111
+ type: "Query",
8112
+ schema: z.array(z.string()).describe("Order items by property values").optional()
8113
+ },
8114
+ {
8115
+ name: "$select",
8116
+ type: "Query",
8117
+ schema: z.array(z.string()).describe("Select properties to be returned").optional()
8118
+ },
8119
+ {
8120
+ name: "$expand",
8121
+ type: "Query",
8122
+ schema: z.array(z.string()).describe("Expand related entities").optional()
8123
+ }
8124
+ ],
8125
+ response: z.void()
8126
+ },
8127
+ {
8128
+ method: "delete",
8129
+ path: "/teams/:teamId/tags/:teamworkTagId/members/:teamworkTagMemberId",
8130
+ alias: "remove-team-tag-member",
8131
+ description: `Delete a member from a standard tag in a team.`,
8132
+ requestFormat: "json",
8133
+ parameters: [
8134
+ {
8135
+ name: "If-Match",
8136
+ type: "Header",
8137
+ schema: z.string().describe("ETag").optional()
8138
+ }
8139
+ ],
8140
+ response: z.void()
8141
+ },
7927
8142
  {
7928
8143
  method: "get",
7929
8144
  path: "/users",
@@ -1,10 +1,10 @@
1
- 2026-03-25 12:50:25 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
2
- 2026-03-25 12:50:25 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
3
- 2026-03-25 12:50:25 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
4
- 2026-03-25 12:50:25 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/messages
5
- 2026-03-25 12:50:25 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/calendar
6
- 2026-03-25 12:50:26 INFO: Using environment variables for secrets
7
- 2026-03-25 12:50:26 INFO: Using environment variables for secrets
8
- 2026-03-25 12:50:26 INFO: Using environment variables for secrets
9
- 2026-03-25 12:50:26 INFO: Using environment variables for secrets
10
- 2026-03-25 12:50:26 INFO: Using environment variables for secrets
1
+ 2026-03-25 16:38:47 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
2
+ 2026-03-25 16:38:47 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
3
+ 2026-03-25 16:38:47 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me
4
+ 2026-03-25 16:38:48 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/messages
5
+ 2026-03-25 16:38:48 INFO: [GRAPH CLIENT] Final URL being sent to Microsoft: https://graph.microsoft.com/v1.0/me/calendar
6
+ 2026-03-25 16:38:49 INFO: Using environment variables for secrets
7
+ 2026-03-25 16:38:49 INFO: Using environment variables for secrets
8
+ 2026-03-25 16:38:49 INFO: Using environment variables for secrets
9
+ 2026-03-25 16:38:49 INFO: Using environment variables for secrets
10
+ 2026-03-25 16:38:49 INFO: Using environment variables for secrets
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.47.0",
3
+ "version": "0.47.2",
4
4
  "description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -658,6 +658,55 @@
658
658
  "toolName": "list-team-members",
659
659
  "workScopes": ["TeamMember.Read.All"]
660
660
  },
661
+ {
662
+ "pathPattern": "/teams/{team-id}/tags",
663
+ "method": "get",
664
+ "toolName": "list-team-tags",
665
+ "workScopes": ["TeamworkTag.Read"],
666
+ "llmTip": "List all tags in a team. Returns tag id, displayName, memberCount, and tagType (standard or scheduled). Use list-joined-teams to find the team ID first."
667
+ },
668
+ {
669
+ "pathPattern": "/teams/{team-id}/tags",
670
+ "method": "post",
671
+ "toolName": "create-team-tag",
672
+ "workScopes": ["TeamworkTag.ReadWrite"],
673
+ "llmTip": "Create a new tag in a team. Body requires displayName (max 40 chars) and an optional members array (max 25 at creation, each with a userId). Use list-team-members to find user IDs. Add more members individually with add-team-tag-member after creation."
674
+ },
675
+ {
676
+ "pathPattern": "/teams/{team-id}/tags/{teamworkTag-id}",
677
+ "method": "get",
678
+ "toolName": "get-team-tag",
679
+ "workScopes": ["TeamworkTag.Read"],
680
+ "llmTip": "Get details of a specific tag including displayName, memberCount, and tagType. Use list-team-tags to find the tag ID."
681
+ },
682
+ {
683
+ "pathPattern": "/teams/{team-id}/tags/{teamworkTag-id}",
684
+ "method": "patch",
685
+ "toolName": "update-team-tag",
686
+ "workScopes": ["TeamworkTag.ReadWrite"],
687
+ "llmTip": "Update a tag's displayName (max 40 chars). Use list-team-tags to find the tag ID."
688
+ },
689
+ {
690
+ "pathPattern": "/teams/{team-id}/tags/{teamworkTag-id}",
691
+ "method": "delete",
692
+ "toolName": "delete-team-tag",
693
+ "workScopes": ["TeamworkTag.ReadWrite"],
694
+ "llmTip": "Permanently delete a tag from a team. This action is irreversible. Use list-team-tags to find the tag ID."
695
+ },
696
+ {
697
+ "pathPattern": "/teams/{team-id}/tags/{teamworkTag-id}/members",
698
+ "method": "get",
699
+ "toolName": "list-team-tag-members",
700
+ "workScopes": ["TeamworkTag.Read"],
701
+ "llmTip": "List all members of a specific tag. Returns member id, displayName, tenantId, and userId. Use list-team-tags to find the tag ID."
702
+ },
703
+ {
704
+ "pathPattern": "/teams/{team-id}/tags/{teamworkTag-id}/members/{teamworkTagMember-id}",
705
+ "method": "delete",
706
+ "toolName": "remove-team-tag-member",
707
+ "workScopes": ["TeamworkTag.ReadWrite"],
708
+ "llmTip": "Remove a member from a tag. Use list-team-tag-members to find the member ID."
709
+ },
661
710
  {
662
711
  "pathPattern": "/chats/{chat-id}/messages/{chatMessage-id}/replies",
663
712
  "method": "get",