Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/mcp-provider-devops/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const TelemetryEventNames = {
CREATE_PULL_REQUEST: 'devops_create_pull_request',
DETECT_CONFLICT: 'devops_detect_conflict',
RESOLVE_CONFLICT: 'devops_resolve_conflict',
UPDATE_WORK_ITEM_STATUS: 'devops_update_work_item_status',
CREATE_WORK_ITEM: 'devops_create_work_item',
} as const;

export const TelemetrySource = 'MCP-DevOps';
Expand Down
67 changes: 67 additions & 0 deletions packages/mcp-provider-devops/src/createWorkItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import axios from "axios";
import { getConnection } from "./shared/auth.js";

const API_VERSION = "v65.0";

export interface CreateWorkItemParams {
usernameOrAlias: string;
projectId: string;
subject: string;
description: string;
}

export interface CreateWorkItemResult {
success: boolean;
workItemId?: string;
workItemName?: string;
subject?: string;
error?: string;
}

/**
* Creates a new DevOps Center Work Item in the specified project.
* API: POST /services/data/v65.0/connect/devops/projects/<ProjectID>/workitem
* Body: { subject: string, description: string }
*/
export async function createWorkItem(params: CreateWorkItemParams): Promise<CreateWorkItemResult> {
const { usernameOrAlias, projectId, subject, description } = params;

const connection = await getConnection(usernameOrAlias);
const accessToken = connection.accessToken;
const instanceUrl = connection.instanceUrl;
if (!accessToken || !instanceUrl) {
return {
success: false,
error: "Missing access token or instance URL.",
};
}

const url = `${instanceUrl}/services/data/${API_VERSION}/connect/devops/projects/${projectId}/workitem`;
const headers = {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
};
const body = { subject, description };

try {
const response = await axios.post(url, body, { headers });
const data = response.data ?? {};
return {
success: true,
workItemId: data.id ?? data.Id,
workItemName: data.name ?? data.Name,
subject: data.subject ?? data.Subject ?? subject,
};
} catch (error: any) {
const data = error.response?.data;
const message =
(typeof data === "object" && (data?.message ?? data?.error ?? data?.errorDescription)) ??
error.message ??
"Unknown error";
const details = Array.isArray(data?.body) ? data.body.join("; ") : undefined;
return {
success: false,
error: details ? `${message}: ${details}` : String(message),
};
}
}
4 changes: 4 additions & 0 deletions packages/mcp-provider-devops/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { CheckCommitStatus } from "./tools/checkCommitStatus.js";
import { CreatePullRequest } from "./tools/createPullRequest.js";
import { SfDevopsCheckoutWorkItem } from "./tools/sfDevopsCheckoutWorkItem.js";
import { SfDevopsCommitWorkItem } from "./tools/sfDevopsCommitWorkItem.js";
import { SfDevopsUpdateWorkItemStatus } from "./tools/sfDevopsUpdateWorkItemStatus.js";
import { SfDevopsCreateWorkItem } from "./tools/sfDevopsCreateWorkItem.js";

/**
* DevOps MCPProvider for DevOps tools and operations
Expand All @@ -22,12 +24,14 @@ export class DevOpsMcpProvider extends McpProvider {
return Promise.resolve([
new SfDevopsListProjects(services),
new SfDevopsListWorkItems(services),
new SfDevopsCreateWorkItem(services),
new SfDevopsPromoteWorkItem(services),
new SfDevopsDetectConflict(telemetryService),
new SfDevopsResolveConflict(telemetryService),

new SfDevopsCheckoutWorkItem(services),
new SfDevopsCommitWorkItem(services),
new SfDevopsUpdateWorkItemStatus(services),

new CheckCommitStatus(telemetryService),
new CreatePullRequest(telemetryService),
Expand Down
113 changes: 113 additions & 0 deletions packages/mcp-provider-devops/src/tools/sfDevopsCreateWorkItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { McpTool, McpToolConfig, ReleaseState, Toolset, Services } from "@salesforce/mcp-provider-api";
import { createWorkItem } from "../createWorkItem.js";
import { TelemetryEventNames } from "../constants.js";
import { usernameOrAliasParam } from "../shared/params.js";

const inputSchema = z.object({
usernameOrAlias: usernameOrAliasParam,
projectId: z.string().min(1).describe("DevOps Center Project ID selected from list_devops_center_projects for the same org."),
subject: z.string().min(1).describe("Work item subject."),
description: z.string().describe("Work item description."),
});
type InputArgs = z.infer<typeof inputSchema>;
type InputArgsShape = typeof inputSchema.shape;
type OutputArgsShape = z.ZodRawShape;

export class SfDevopsCreateWorkItem extends McpTool<InputArgsShape, OutputArgsShape> {
private readonly services: Services;

constructor(services: Services) {
super();
this.services = services;
}

public getReleaseState(): ReleaseState {
return ReleaseState.NON_GA;
}

public getToolsets(): Toolset[] {
return [Toolset.DEVOPS];
}

public getName(): string {
return "create_devops_center_work_item";
}

public getConfig(): McpToolConfig<InputArgsShape, OutputArgsShape> {
return {
title: "Create Work Item",
description: `Creates a new DevOps Center Work Item in the specified project.

**Usage notes:**
- This tool must be used for the DevOps Center org only. If the org is not provided, use 'list_all_orgs' to select the DevOps Center org.
- A DevOps Center project must be selected first from the same org. If the projectId is not known, call 'list_devops_center_projects' for that org and ask the user to select a project. Use that project's Id here.
- Ensure the org used to select the project is the same org passed to this tool.
- (**Mandatory) Always ask the user to give the work item subject. Don't proceed until the user has provided the subject.(**Mandatory**)**

**API:** POST /services/data/v65.0/connect/devops/projects/<ProjectID>/workitem
**Body:** { "subject": string, "description": string }

**Input parameters:**
- usernameOrAlias: DevOps Center org username or alias. If missing, use 'list_all_orgs' and ask user to select the DevOps Center org.
- projectId: DevOps Center Project ID from list_devops_center_projects for the same org.
- subject: Work item subject.
- description: Work item description.

**Output:**
- success: Whether the create succeeded.
- workItemId, workItemName, subject: Created work item details on success.
- error: Error message if the create failed.`,
inputSchema: inputSchema.shape,
outputSchema: undefined,
};
}

public async exec(input: InputArgs): Promise<CallToolResult> {
const startTime = Date.now();

try {
const result = await createWorkItem({
usernameOrAlias: input.usernameOrAlias,
projectId: input.projectId,
subject: input.subject,
description: input.description,
});

const executionTime = Date.now() - startTime;

this.services.getTelemetryService().sendEvent(TelemetryEventNames.CREATE_WORK_ITEM, {
success: result.success,
projectId: input.projectId,
executionTimeMs: executionTime,
...(result.error && { error: result.error }),
});

if (!result.success) {
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
isError: true,
};
}

return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (e: any) {
const executionTime = Date.now() - startTime;

this.services.getTelemetryService().sendEvent(TelemetryEventNames.CREATE_WORK_ITEM, {
success: false,
error: e?.message || "Unknown error",
projectId: input.projectId,
executionTimeMs: executionTime,
});

return {
content: [{ type: "text", text: `Error creating work item: ${e?.message || e}` }],
isError: true,
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { McpTool, McpToolConfig, ReleaseState, Toolset, Services } from "@salesforce/mcp-provider-api";
import { updateWorkItemStatus, type WorkItemStatus } from "../updateWorkItemStatus.js";
import { TelemetryEventNames } from "../constants.js";
import { usernameOrAliasParam } from "../shared/params.js";

const statusEnum = z.enum(["In Progress", "Ready to Promote"]);

const inputSchema = z.object({
usernameOrAlias: usernameOrAliasParam,
workItemName: z.string().min(1).describe("Exact Work Item Name to update (e.g. WI-00000001)."),
status: statusEnum.describe("New status: 'In Progress' or 'Ready to Promote'"),
});
type InputArgs = z.infer<typeof inputSchema>;
type InputArgsShape = typeof inputSchema.shape;
type OutputArgsShape = z.ZodRawShape;

export class SfDevopsUpdateWorkItemStatus extends McpTool<InputArgsShape, OutputArgsShape> {
private readonly services: Services;

constructor(services: Services) {
super();
this.services = services;
}

public getReleaseState(): ReleaseState {
return ReleaseState.NON_GA;
}

public getToolsets(): Toolset[] {
return [Toolset.DEVOPS];
}

public getName(): string {
return "update_devops_center_work_item_status";
}

public getConfig(): McpToolConfig<InputArgsShape, OutputArgsShape> {
return {
title: "Update Work Item Status",
description: `Update the status of a DevOps Center work item to either "In Progress" or "Ready to Promote".

**Use when user asks (examples):**
- "Mark WI-123 In Progress"
- "Set work item WI-456 to Ready to Promote"
- "Change work item status to In Progress"
- "Mark my work item as Ready to Promote"

**Prerequisites:**
- This tool must be used only for the DevOps Center org.
- The user must provide: username (DevOps Center), Work Item Name, and the desired status.

**Input Parameters:**
- usernameOrAlias: DevOps Center org username or alias. If missing, use 'list_all_orgs' and ask user to select the DevOps Center org.
- workItemName: Exact Work Item Name (e.g. WI-00000001).
- status: New status - either "In Progress" or "Ready to Promote".

**Output:**
- success: Whether the update succeeded.
- workItemId, workItemName, status: Updated work item details on success.
- error: Error message if the work item was not found or update failed.

**Next steps:**
- After marking "Ready to Promote", suggest promoting the work item (using 'promote_devops_center_work_item') when appropriate.`,
inputSchema: inputSchema.shape,
outputSchema: undefined,
};
}

public async exec(input: InputArgs): Promise<CallToolResult> {
const startTime = Date.now();

try {
const result = await updateWorkItemStatus(
input.usernameOrAlias,
input.workItemName,
input.status as WorkItemStatus
);

const executionTime = Date.now() - startTime;

this.services.getTelemetryService().sendEvent(TelemetryEventNames.UPDATE_WORK_ITEM_STATUS, {
success: result.success,
workItemName: input.workItemName,
status: input.status,
executionTimeMs: executionTime,
...(result.error && { error: result.error }),
});

if (!result.success) {
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
isError: true,
};
}

return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (e: any) {
const executionTime = Date.now() - startTime;

this.services.getTelemetryService().sendEvent(TelemetryEventNames.UPDATE_WORK_ITEM_STATUS, {
success: false,
error: e?.message || "Unknown error",
workItemName: input.workItemName,
executionTimeMs: executionTime,
});

return {
content: [{ type: "text", text: `Error updating work item status: ${e?.message || e}` }],
isError: true,
};
}
}
}
Loading