@azure-devops/mcp 2.9.0 → 2.10.0-nightly.20260909

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.
@@ -8,6 +8,7 @@ import { z } from "zod";
8
8
  import { batchApiVersion, markdownCommentsApiVersion, getEnumKeys, safeEnumConvert, encodeFormattedValue } from "../utils.js";
9
9
  import { elicitProject, elicitTeam } from "../shared/elicitations.js";
10
10
  import { createExternalContentResponse } from "../shared/content-safety.js";
11
+ import { getUserIdentityFromEmail } from "./auth.js";
11
12
  const WORKITEM_TOOLS = {
12
13
  wit_work_item: "wit_work_item",
13
14
  wit_query: "wit_query",
@@ -43,6 +44,8 @@ function getLinkTypeFromName(name) {
43
44
  return "Microsoft.VSTS.Common.Affects-Reverse";
44
45
  case "artifact":
45
46
  return "ArtifactLink";
47
+ case "hyperlink":
48
+ return "Hyperlink";
46
49
  default:
47
50
  throw new Error(`Unknown link type: ${name}`);
48
51
  }
@@ -55,6 +58,36 @@ function getArtifactLinkAttributeName(linkType) {
55
58
  return linkType;
56
59
  }
57
60
  }
61
+ function escapeHtml(value) {
62
+ const entities = {
63
+ "&": "&",
64
+ "<": "&lt;",
65
+ ">": "&gt;",
66
+ '"': "&quot;",
67
+ "'": "&#39;",
68
+ };
69
+ return value.replace(/[&<>"']/g, (character) => entities[character]);
70
+ }
71
+ async function resolveCommentMentions(text, format, tokenProvider, connectionProvider, userAgentProvider) {
72
+ const emailMatches = [...text.matchAll(/@<([^<>\s]+@[^<>\s]+)>/g)];
73
+ if (emailMatches.length === 0)
74
+ return text;
75
+ const identities = new Map();
76
+ for (const email of new Set(emailMatches.map((match) => match[1]))) {
77
+ try {
78
+ identities.set(email, await getUserIdentityFromEmail(email, tokenProvider, connectionProvider, userAgentProvider));
79
+ }
80
+ catch {
81
+ // Leave mentions unchanged when their identities cannot be resolved.
82
+ }
83
+ }
84
+ return text.replace(/@<([^<>\s]+@[^<>\s]+)>/g, (mention, email) => {
85
+ const identity = identities.get(email);
86
+ if (!identity)
87
+ return escapeHtml(mention);
88
+ return format === "Markdown" || format === undefined ? `@<${identity.id}>` : `<a href="#" data-vss-mention="version:2.0,${identity.id}">@${escapeHtml(identity.displayName)}</a>`;
89
+ });
90
+ }
58
91
  function configureWorkItemTools(server, tokenProvider, connectionProvider, userAgentProvider) {
59
92
  // --- wit_work_item ----------------------------------------------------------
60
93
  server.tool(WORKITEM_TOOLS.wit_work_item, "Retrieve work item data for a project. Use the action parameter to specify the operation.", {
@@ -309,11 +342,24 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
309
342
  });
310
343
  // --- wit_backlog ------------------------------------------------------------
311
344
  server.tool(WORKITEM_TOOLS.wit_backlog, "Retrieve backlog data for a project and team. Use the action parameter to specify the operation.", {
312
- action: z.enum(["list", "list_work_items"]).describe("The action to perform. Options: list (list backlog levels for a team), list_work_items (list work items in a specific backlog level)."),
345
+ action: z
346
+ .enum(["list", "list_work_items", "reorder"])
347
+ .describe("The action to perform. Options: list (list backlog levels for a team), list_work_items (list work items in a specific backlog level), reorder (move work items to a new position in a backlog or iteration)."),
313
348
  project: z.string().optional().describe("The name or ID of the Azure DevOps project. Reuse from prior context if already known. If not provided, a project selection prompt will be shown."),
314
349
  team: z.string().optional().describe("The name or ID of the Azure DevOps team. Reuse from prior context if already known. If not provided, a team selection prompt will be shown."),
315
350
  backlogId: z.string().optional().describe("The ID of the backlog category to retrieve work items from. Required for: list_work_items."),
316
- }, async ({ action, project, team, backlogId }) => {
351
+ ids: z.array(z.coerce.number().int().min(1)).min(1).optional().describe("The IDs of the work items to reorder. Required for: reorder."),
352
+ previousId: z.coerce
353
+ .number()
354
+ .int()
355
+ .min(0)
356
+ .optional()
357
+ .describe("The ID of the work item that should be before the reordered items. Use 0 to specify the beginning of the list. Optional for: reorder."),
358
+ nextId: z.coerce.number().int().min(0).optional().describe("The ID of the work item that should be after the reordered items. Use 0 to specify the end of the list. Optional for: reorder."),
359
+ parentId: z.coerce.number().int().min(0).optional().describe("The parent ID for all work items involved in the operation. Use 0 to indicate the items have no parent. Optional for: reorder."),
360
+ iterationPath: z.string().optional().describe("The iteration path for the reorder operation. Used when reordering items in an iteration backlog. Optional for: reorder."),
361
+ iterationId: z.string().optional().describe("The iteration ID. When provided, reorder items in that iteration instead of the team backlog. Used for: reorder."),
362
+ }, async ({ action, project, team, backlogId, ids, previousId, nextId, parentId, iterationPath, iterationId }) => {
317
363
  try {
318
364
  const connection = await connectionProvider();
319
365
  let resolvedProject = project;
@@ -344,6 +390,13 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
344
390
  const workItems = await workApi.getBacklogLevelWorkItems(teamContext, backlogId);
345
391
  return { content: [{ type: "text", text: JSON.stringify(workItems, null, 2) }] };
346
392
  }
393
+ if (action === "reorder") {
394
+ if (!ids?.length)
395
+ return { content: [{ type: "text", text: "ids is required for reorder" }], isError: true };
396
+ const operation = { ids, previousId, nextId, parentId, iterationPath };
397
+ const reorderedItems = iterationId ? await workApi.reorderIterationWorkItems(operation, teamContext, iterationId) : await workApi.reorderBacklogWorkItems(operation, teamContext);
398
+ return { content: [{ type: "text", text: JSON.stringify(reorderedItems, null, 2) }] };
399
+ }
347
400
  return { content: [{ type: "text", text: `Unknown action: ${action}` }], isError: true };
348
401
  }
349
402
  catch (error) {
@@ -351,6 +404,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
351
404
  const msgs = {
352
405
  list: `Error listing backlogs: ${errorMessage}`,
353
406
  list_work_items: `Error listing backlog work items: ${errorMessage}`,
407
+ reorder: `Error reordering backlog work items: ${errorMessage}`,
354
408
  };
355
409
  return { content: [{ type: "text", text: msgs[action] ?? `Error: ${errorMessage}` }], isError: true };
356
410
  }
@@ -404,9 +458,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
404
458
  }
405
459
  const mimeType = getMimeType(fileName);
406
460
  if (mimeType.startsWith("text/")) {
407
- return {
408
- content: [{ type: "text", text: buffer.toString("utf-8") }],
409
- };
461
+ return createExternalContentResponse(buffer.toString("utf-8"), "work item attachment");
410
462
  }
411
463
  const base64Data = buffer.toString("base64");
412
464
  return {
@@ -451,14 +503,17 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
451
503
  op: z
452
504
  .string()
453
505
  .transform((val) => val.toLowerCase())
454
- .pipe(z.enum(["add", "replace", "remove"]))
506
+ .pipe(z.enum(["add", "replace", "remove", "test"]))
455
507
  .default("add")
456
- .describe("The operation to perform."),
457
- path: z.string().describe("The field path, e.g. '/fields/System.Title'."),
458
- value: z.string().describe("The new value for the field."),
508
+ .describe("The operation to perform. Use 'test' with path '/rev' to enforce optimistic concurrency."),
509
+ path: z.string().describe("The path to operate on, e.g. '/fields/System.Title' or '/rev' for a revision test."),
510
+ value: z
511
+ .union([z.string(), z.number(), z.boolean(), z.null()])
512
+ .optional()
513
+ .describe("The operation value. Required for add, replace, and test; omit for remove. For a test on '/rev', pass the numeric revision previously read."),
459
514
  }))
460
515
  .optional()
461
- .describe("Field updates for a single work item. Required for: update."),
516
+ .describe('Field updates for a single work item. Required for: update. For a safe read-modify-write, prepend a test operation on "/rev" with value set to the numeric revision returned by the preceding read; Azure DevOps rejects the entire update if the current revision differs.'),
462
517
  batchUpdates: z
463
518
  .array(z.object({
464
519
  op: z.enum(["Add", "Replace", "Remove"]).default("Add").describe("The operation to perform."),
@@ -521,6 +576,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
521
576
  return { content: [{ type: "text", text: "id is required for update" }], isError: true };
522
577
  if (!updates || updates.length === 0)
523
578
  return { content: [{ type: "text", text: "updates is required for update" }], isError: true };
579
+ const updateWithoutValue = updates.find((update) => update.op !== "remove" && update.value === undefined);
580
+ if (updateWithoutValue) {
581
+ return { content: [{ type: "text", text: `value is required for ${updateWithoutValue.op}` }], isError: true };
582
+ }
524
583
  const workItemApi = await connection.getWorkItemTrackingApi();
525
584
  const apiUpdates = updates.map((update) => ({ ...update, op: update.op }));
526
585
  const updatedWorkItem = await workItemApi.updateWorkItem(null, apiUpdates, id);
@@ -593,8 +652,6 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
593
652
  const ops = [
594
653
  { op: "add", path: "/id", value: `-${x + 1}` },
595
654
  { op: "add", path: "/fields/System.Title", value: item.title },
596
- { op: "add", path: "/fields/System.Description", value: encodedDescription },
597
- { op: "add", path: "/fields/Microsoft.VSTS.TCM.ReproSteps", value: encodedDescription },
598
655
  {
599
656
  op: "add",
600
657
  path: "/relations/-",
@@ -610,9 +667,19 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
610
667
  if (item.iterationPath && item.iterationPath.trim().length > 0) {
611
668
  ops.push({ op: "add", path: "/fields/System.IterationPath", value: item.iterationPath });
612
669
  }
613
- if (item.format && item.format === "Markdown") {
614
- ops.push({ op: "add", path: "/multilineFieldsFormat/System.Description", value: item.format });
615
- ops.push({ op: "add", path: "/multilineFieldsFormat/Microsoft.VSTS.TCM.ReproSteps", value: item.format });
670
+ // check if the work item type is "Bug" to determine which field to use for the description
671
+ // ReproSteps is used for Bugs, while Description is used for other work item types
672
+ if (workItemType.toLowerCase() === "bug") {
673
+ ops.push({ op: "add", path: "/fields/Microsoft.VSTS.TCM.ReproSteps", value: encodedDescription });
674
+ if (item.format && item.format === "Markdown") {
675
+ ops.push({ op: "add", path: "/multilineFieldsFormat/Microsoft.VSTS.TCM.ReproSteps", value: item.format });
676
+ }
677
+ }
678
+ else {
679
+ ops.push({ op: "add", path: "/fields/System.Description", value: encodedDescription });
680
+ if (item.format && item.format === "Markdown") {
681
+ ops.push({ op: "add", path: "/multilineFieldsFormat/System.Description", value: item.format });
682
+ }
616
683
  }
617
684
  return {
618
685
  method: "PATCH",
@@ -641,9 +708,12 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
641
708
  }
642
709
  catch (error) {
643
710
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
711
+ const statusCode = typeof error === "object" && error !== null && "statusCode" in error && typeof error.statusCode === "number" ? error.statusCode : undefined;
712
+ const statusText = statusCode === 409 ? " Conflict" : statusCode === 412 ? " Precondition Failed" : "";
713
+ const updateStatus = statusCode !== undefined ? ` [HTTP ${statusCode}${statusText}]` : "";
644
714
  const msgs = {
645
715
  create: `Error creating work item: ${errorMessage}`,
646
- update: `Error updating work item: ${errorMessage}`,
716
+ update: `Error updating work item${updateStatus}: ${errorMessage}`,
647
717
  update_batch: `Error updating work items in batch: ${errorMessage}`,
648
718
  add_child: `Error creating child work items: ${errorMessage}`,
649
719
  };
@@ -676,6 +746,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
676
746
  const orgUrl = connection.serverUrl;
677
747
  const accessToken = await tokenProvider();
678
748
  const formatParameter = (format ?? "Markdown") === "Markdown" ? 0 : 1;
749
+ const resolvedText = await resolveCommentMentions(text, format, tokenProvider, connectionProvider, userAgentProvider);
679
750
  if (action === "add") {
680
751
  const response = await fetch(`${orgUrl}/${encodeURIComponent(resolvedProject)}/_apis/wit/workItems/${workItemId}/comments?format=${formatParameter}&api-version=${markdownCommentsApiVersion}`, {
681
752
  method: "POST",
@@ -684,7 +755,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
684
755
  "Content-Type": "application/json",
685
756
  "User-Agent": userAgentProvider(),
686
757
  },
687
- body: JSON.stringify({ text }),
758
+ body: JSON.stringify({ text: resolvedText }),
688
759
  });
689
760
  if (!response.ok) {
690
761
  throw new Error(`Failed to add a work item comment: ${response.statusText}`);
@@ -701,7 +772,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
701
772
  "Content-Type": "application/json",
702
773
  "User-Agent": userAgentProvider(),
703
774
  },
704
- body: JSON.stringify({ text }),
775
+ body: JSON.stringify({ text: resolvedText }),
705
776
  });
706
777
  if (!response.ok) {
707
778
  throw new Error(`Failed to update work item comment: ${response.statusText}`);
@@ -729,9 +800,10 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
729
800
  updates: z
730
801
  .array(z.object({
731
802
  id: z.coerce.number().min(1).describe("The ID of the work item to update."),
732
- linkToId: z.coerce.number().min(1).describe("The ID of the work item to link to."),
803
+ linkToId: z.coerce.number().min(1).optional().describe("The ID of the work item to link to. Required unless type is 'hyperlink'."),
804
+ url: z.string().optional().describe("The URL for a hyperlink. Required when type is 'hyperlink'."),
733
805
  type: z
734
- .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by"])
806
+ .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "hyperlink"])
735
807
  .default("related")
736
808
  .describe("Type of link. Defaults to 'related'."),
737
809
  comment: z.string().optional().describe("Optional comment for the link."),
@@ -741,7 +813,7 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
741
813
  // unlink
742
814
  id: z.coerce.number().min(1).optional().describe("Work item ID to remove links from. Required for: unlink."),
743
815
  type: z
744
- .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "artifact"])
816
+ .enum(["parent", "child", "duplicate", "duplicate of", "related", "successor", "predecessor", "tested by", "tests", "affects", "affected by", "artifact", "hyperlink"])
745
817
  .optional()
746
818
  .describe("Link type to remove. Required for: unlink."),
747
819
  url: z.string().optional().describe("URL to match when removing a link. Used for: unlink. If not provided, all links of the specified type are removed."),
@@ -801,15 +873,23 @@ function configureWorkItemTools(server, tokenProvider, connectionProvider, userA
801
873
  headers: { "Content-Type": "application/json-patch+json" },
802
874
  body: updates
803
875
  .filter((update) => update.id === uid)
804
- .map(({ linkToId, type: linkTypeName, comment: linkComment }) => ({
805
- op: "add",
806
- path: "/relations/-",
807
- value: {
808
- rel: `${getLinkTypeFromName(linkTypeName)}`,
809
- url: `${orgUrl}/${resolvedProject}/_apis/wit/workItems/${linkToId}`,
810
- attributes: { comment: linkComment || "" },
811
- },
812
- })),
876
+ .map(({ linkToId, url: linkUrl, type: linkTypeName, comment: linkComment }) => {
877
+ if (linkTypeName === "hyperlink" && !linkUrl) {
878
+ throw new Error("url is required for hyperlink links");
879
+ }
880
+ if (linkTypeName !== "hyperlink" && !linkToId) {
881
+ throw new Error("linkToId is required for work item links");
882
+ }
883
+ return {
884
+ op: "add",
885
+ path: "/relations/-",
886
+ value: {
887
+ rel: getLinkTypeFromName(linkTypeName),
888
+ url: linkTypeName === "hyperlink" ? linkUrl : `${orgUrl}/${resolvedProject}/_apis/wit/workItems/${linkToId}`,
889
+ attributes: { comment: linkComment || "" },
890
+ },
891
+ };
892
+ }),
813
893
  }));
814
894
  const response = await fetch(`${orgUrl}/_apis/wit/$batch?api-version=${batchApiVersion}`, {
815
895
  method: "PATCH",
package/dist/tools.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
+ import { wrapExternalToolResponse } from "./shared/content-safety.js";
3
4
  import { Domain } from "./shared/domains.js";
4
5
  import { configureAdvSecTools } from "./tools/advanced-security.js";
5
6
  import { configureMcpAppsTools } from "./tools/mcp-apps.js";
@@ -14,11 +15,13 @@ import { configureWorkItemTools } from "./tools/work-items.js";
14
15
  function configureAllTools(server, tokenProvider, connectionProvider, userAgentProvider, enabledDomains) {
15
16
  const configureIfDomainEnabled = (domain, configureFn) => {
16
17
  if (enabledDomains.has(domain)) {
17
- configureFn();
18
+ configureToolsWithContentSafety(server, domain, configureFn);
18
19
  }
19
20
  };
20
21
  configureIfDomainEnabled(Domain.CORE, () => configureCoreTools(server, tokenProvider, connectionProvider, userAgentProvider));
21
- configureIfDomainEnabled(Domain.MCP_APPS, () => configureMcpAppsTools(server));
22
+ // This is a local health-check response and contains no Azure DevOps content.
23
+ if (enabledDomains.has(Domain.MCP_APPS))
24
+ configureMcpAppsTools(server);
22
25
  configureIfDomainEnabled(Domain.WORK, () => configureWorkTools(server, tokenProvider, connectionProvider));
23
26
  configureIfDomainEnabled(Domain.PIPELINES, () => configurePipelineTools(server, tokenProvider, connectionProvider, userAgentProvider));
24
27
  configureIfDomainEnabled(Domain.REPOSITORIES, () => configureRepoTools(server, tokenProvider, connectionProvider, userAgentProvider));
@@ -28,4 +31,34 @@ function configureAllTools(server, tokenProvider, connectionProvider, userAgentP
28
31
  configureIfDomainEnabled(Domain.SEARCH, () => configureSearchTools(server, tokenProvider, connectionProvider, userAgentProvider));
29
32
  configureIfDomainEnabled(Domain.ADVANCED_SECURITY, () => configureAdvSecTools(server, tokenProvider, connectionProvider));
30
33
  }
34
+ /**
35
+ * Centralizes the untrusted-content boundary for tool responses. Tool registration
36
+ * is synchronous, so the original method is restored before this function returns.
37
+ */
38
+ function configureToolsWithContentSafety(server, domain, configureFn) {
39
+ const originalTool = server.tool;
40
+ const originalRegisterTool = server.registerTool;
41
+ const wrapRegistrationMethod = (registrationMethod) => new Proxy(registrationMethod, {
42
+ apply(target, thisArg, argumentsList) {
43
+ const callbackIndex = argumentsList.length - 1;
44
+ const callback = argumentsList[callbackIndex];
45
+ if (typeof callback === "function") {
46
+ argumentsList[callbackIndex] = async (...callbackArgs) => {
47
+ const response = (await Reflect.apply(callback, undefined, callbackArgs));
48
+ return wrapExternalToolResponse(response, `Azure DevOps ${domain}`);
49
+ };
50
+ }
51
+ return Reflect.apply(target, thisArg, argumentsList);
52
+ },
53
+ });
54
+ server.tool = wrapRegistrationMethod(originalTool);
55
+ server.registerTool = wrapRegistrationMethod(originalRegisterTool);
56
+ try {
57
+ configureFn();
58
+ }
59
+ finally {
60
+ server.tool = originalTool;
61
+ server.registerTool = originalRegisterTool;
62
+ }
63
+ }
31
64
  export { configureAllTools };
package/dist/useragent.js CHANGED
File without changes
package/dist/utils.js CHANGED
@@ -3,6 +3,19 @@
3
3
  export const apiVersion = "7.2-preview.1";
4
4
  export const batchApiVersion = "5.0";
5
5
  export const markdownCommentsApiVersion = "7.2-preview.4";
6
+ /**
7
+ * Returns the user-supplied CLI arguments.
8
+ *
9
+ * The server is always started script-style — `[runtime, scriptPath, ...args]` — on Node
10
+ * and on Electron hosts alike, so the first two entries are always dropped.
11
+ *
12
+ * Do not replace this with yargs' `hideBin`: it drops a single entry whenever
13
+ * `process.versions.electron` is set and `process.defaultApp` is not, so on an Electron
14
+ * host the script path survives and is parsed as the organization name.
15
+ */
16
+ export function getCliArgs(argv = process.argv) {
17
+ return argv.slice(2);
18
+ }
6
19
  export function createEnumMapping(enumObject) {
7
20
  const mapping = {};
8
21
  for (const [key, value] of Object.entries(enumObject)) {
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const packageVersion = "2.9.0";
1
+ export const packageVersion = "2.10.0-nightly.20260909";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-devops/mcp",
3
- "version": "2.9.0",
3
+ "version": "2.10.0-nightly.20260909",
4
4
  "mcpName": "microsoft.com/azure-devops",
5
5
  "description": "MCP server for interacting with Azure DevOps",
6
6
  "license": "MIT",
@@ -37,12 +37,15 @@
37
37
  "test": "jest"
38
38
  },
39
39
  "dependencies": {
40
- "@azure/identity": "^4.10.0",
41
- "@azure/msal-node": "^5.0.6",
40
+ "@azure/identity": "^4.13.0",
41
+ "@azure/logger": "^1.3.0",
42
+ "@azure/msal-node": "^5.5.0",
43
+ "@azure/msal-node-extensions": "^5.3.5",
42
44
  "@modelcontextprotocol/sdk": "1.29.0",
43
45
  "azure-devops-extension-api": "^5.272.3",
44
46
  "azure-devops-extension-sdk": "^4.0.2",
45
47
  "azure-devops-node-api": "^15.1.2",
48
+ "open": "^10.2.0",
46
49
  "winston": "^3.18.3",
47
50
  "yargs": "^18.0.0",
48
51
  "zod": "^3.25.63",
@@ -69,5 +72,9 @@
69
72
  "**/*.(js|ts|jsx|tsx|json|css|md)": [
70
73
  "npm run format"
71
74
  ]
75
+ },
76
+ "allowScripts": {
77
+ "keytar@7.9.0": true,
78
+ "@azure/msal-node-extensions@5.3.5": true
72
79
  }
73
80
  }
@@ -1,34 +0,0 @@
1
- // Copyright (c) Microsoft Corporation.
2
- // Licensed under the MIT License.
3
- /** Builds an error `CallToolResult`. */
4
- export const errorResult = (text) => ({ content: [{ type: "text", text }], isError: true });
5
- /**
6
- * Routes a validated, action-carrying args object to the matching command.
7
- *
8
- * This is what removes long positional parameter lists from grouped ("action")
9
- * tools: instead of destructuring every possible field, the whole typed args
10
- * object is forwarded to the single command keyed by `args.action`, coupling
11
- * each action to exactly one command.
12
- *
13
- * - Unknown actions short-circuit with an "Unknown action" error and never
14
- * touch the context (so no connection is opened).
15
- * - Errors thrown by a command are caught and formatted using the optional
16
- * per-action `errorPrefixes` map (falling back to a generic message).
17
- * - Errors returned by a command (e.g. validation `errorResult`s) pass through
18
- * unchanged.
19
- */
20
- export async function dispatchAction(commands, context, args, errorPrefixes) {
21
- const command = commands[args.action];
22
- if (!command) {
23
- const supportedActions = Object.keys(commands).sort().join(", ");
24
- return errorResult(`Unknown action: ${args.action}. Supported actions: ${supportedActions}`);
25
- }
26
- try {
27
- return await command.execute(context, args);
28
- }
29
- catch (error) {
30
- const message = error instanceof Error ? error.message : "Unknown error occurred";
31
- const prefix = errorPrefixes?.[args.action];
32
- return errorResult(prefix ? `${prefix}${message}` : `Error: ${message}`);
33
- }
34
- }