Skip to main content
Glama
Tiberriver256

Azure DevOps MCP Server

create_commit

Create commits with file changes in Azure DevOps repositories using search/replace or unified diff formats to manage code updates.

Instructions

Create a commit on an existing branch using file changes.

  • Provide plain branch names (no "refs/heads/").

  • ⚠️ Each file path may appear only once per commit request—combine all edits to a file into a single change entry.

  • Prefer multiple commits when you have sparse or unrelated edits; smaller focused commits keep review context clear.

🎯 RECOMMENDED: Use the SEARCH/REPLACE format (much easier, no line counting!).

Option 1: SEARCH/REPLACE format (EASIEST) Simply provide the exact text to find and replace:

{
  "changes": [{
    "path": "src/api/services/function-call.ts",
    "search": "return axios.post(apiUrl, payload, requestConfig);",
    "replace": "return axios.post(apiUrl, payload, requestConfig).then(r => { processResponse(r); return r; });"
  }]
}

The server fetches the file, performs the replacement, and generates the diff automatically. No line counting, no hunk headers, no context lines needed!

Option 2: UNIFIED DIFF format (Advanced) If you prefer full control, provide complete unified diffs:

  • Each patch MUST have complete hunk headers: @@ -oldStart,oldLines +newStart,newLines @@

  • CRITICAL: Every @@ marker MUST include line numbers. Do NOT use @@ without line ranges.

  • Include 3-5 context lines before and after changes.

  • For deletions: --- a/filepath and +++ /dev/null

  • For additions: --- /dev/null and +++ b/filepath

Example unified diff:

{
  "changes": [{
    "patch": "diff --git a/file.yaml b/file.yaml\n--- a/file.yaml\n+++ b/file.yaml\n@@ -4,7 +4,7 @@ spec:\n spec:\n   type: ClusterIP\n   ports:\n-    - port: 8080\n+    - port: 9090\n       targetPort: http\n"
  }]
}

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
projectIdNoThe ID or name of the project (Default: MyProject)
organizationIdNoThe ID or name of the organization (Default: mycompany)
repositoryIdYesThe ID or name of the repository
branchNameYesThe branch to commit to (without "refs/heads/", e.g., "codex/test2-delete-main-py")
commitMessageYesCommit message
changesYesList of file changes as either unified git diffs OR search/replace pairs

Implementation Reference

  • Core handler function implementing the create_commit tool. Handles patch application or search/replace, constructs GitChange array, and creates commit via Azure DevOps Git API.
    export async function createCommit(
      connection: WebApi,
      options: CreateCommitOptions,
    ): Promise<void> {
      try {
        const gitApi = await connection.getGitApi();
        const branch = await gitApi.getBranch(
          options.repositoryId,
          options.branchName,
          options.projectId,
        );
        const baseCommit = branch?.commit?.commitId;
        if (!baseCommit) {
          throw new AzureDevOpsError(`Branch '${options.branchName}' not found`);
        }
    
        const changes: GitChange[] = [];
    
        for (const file of options.changes) {
          // Handle search/replace format by generating a patch
          let patchString = file.patch;
    
          if (
            !patchString &&
            file.search !== undefined &&
            file.replace !== undefined
          ) {
            if (!file.path) {
              throw new AzureDevOpsError(
                'path is required when using search/replace format',
              );
            }
    
            // Fetch current file content
            let currentContent = '';
            try {
              const stream = await gitApi.getItemContent(
                options.repositoryId,
                file.path,
                options.projectId,
                undefined,
                undefined,
                undefined,
                undefined,
                false,
                { version: options.branchName, versionType: GitVersionType.Branch },
                true,
              );
              currentContent = stream ? await streamToString(stream) : '';
            } catch {
              // File might not exist (new file scenario) - treat as empty
              currentContent = '';
            }
    
            // Perform the replacement
            if (!currentContent.includes(file.search)) {
              throw new AzureDevOpsError(
                `Search string not found in ${file.path}. The file may have been modified since you last read it.`,
              );
            }
    
            const newContent = currentContent.replace(file.search, file.replace);
    
            // Generate proper unified diff
            patchString = createTwoFilesPatch(
              file.path,
              file.path,
              currentContent,
              newContent,
              undefined,
              undefined,
            );
          }
    
          if (!patchString) {
            throw new AzureDevOpsError(
              'Either patch or both search and replace must be provided for each change',
            );
          }
    
          const patches = parsePatch(patchString);
          if (patches.length !== 1) {
            throw new AzureDevOpsError(
              `Expected a single file diff for change but received ${patches.length}`,
            );
          }
    
          const patch = patches[0];
    
          const normalizePath = (path?: string | null): string | undefined => {
            if (!path || path === '/dev/null') {
              return undefined;
            }
            return path.replace(/^a\//, '').replace(/^b\//, '');
          };
    
          const oldPath = normalizePath(patch.oldFileName);
          const newPath = normalizePath(patch.newFileName);
          const targetPath = file.path ?? newPath ?? oldPath;
    
          if (!targetPath) {
            throw new AzureDevOpsError(
              'Unable to determine target path for change',
            );
          }
    
          if (oldPath && newPath && oldPath !== newPath) {
            throw new AzureDevOpsError(
              `Renaming files is not supported (attempted ${oldPath} -> ${newPath})`,
            );
          }
    
          let originalContent = '';
    
          if (oldPath) {
            const stream = await gitApi.getItemContent(
              options.repositoryId,
              oldPath,
              options.projectId,
              undefined,
              undefined,
              undefined,
              undefined,
              false,
              { version: options.branchName, versionType: GitVersionType.Branch },
              true,
            );
            originalContent = stream ? await streamToString(stream) : '';
          }
    
          const patchedContent = applyPatch(originalContent, patch);
    
          if (patchedContent === false) {
            throw new AzureDevOpsError(
              `Failed to apply diff for ${targetPath}. Please ensure the patch is up to date with the branch head.`,
            );
          }
    
          if (!newPath) {
            changes.push({
              changeType: VersionControlChangeType.Delete,
              item: { path: targetPath },
            });
            continue;
          }
    
          const changeType = oldPath
            ? VersionControlChangeType.Edit
            : VersionControlChangeType.Add;
    
          changes.push({
            changeType,
            item: { path: targetPath },
            newContent: {
              content: patchedContent,
              contentType: ItemContentType.RawText,
            },
          });
        }
    
        const commit = {
          comment: options.commitMessage,
          changes,
        };
    
        const refUpdate: GitRefUpdate = {
          name: `refs/heads/${options.branchName}`,
          oldObjectId: baseCommit,
        };
    
        await gitApi.createPush(
          { commits: [commit], refUpdates: [refUpdate] },
          options.repositoryId,
          options.projectId,
        );
      } catch (error) {
        if (error instanceof AzureDevOpsError) {
          throw error;
        }
        throw new Error(
          `Failed to create commit: ${error instanceof Error ? error.message : String(error)}`,
        );
      }
    }
  • Zod input validation schema for the create_commit tool, defining parameters like repositoryId, branchName, commitMessage, and changes (supporting unified diff patches or search/replace format).
    export const CreateCommitSchema = z
      .object({
        projectId: z
          .string()
          .optional()
          .describe(`The ID or name of the project (Default: ${defaultProject})`),
        organizationId: z
          .string()
          .optional()
          .describe(`The ID or name of the organization (Default: ${defaultOrg})`),
        repositoryId: z.string().describe('The ID or name of the repository'),
        branchName: z
          .string()
          .describe(
            'The branch to commit to (without "refs/heads/", e.g., "codex/test2-delete-main-py")',
          ),
        commitMessage: z.string().describe('Commit message'),
        changes: z
          .array(
            z
              .object({
                path: z
                  .string()
                  .optional()
                  .describe(
                    'File path. Optional for patch format (uses diff header), REQUIRED for search/replace format',
                  ),
                patch: z
                  .string()
                  .optional()
                  .describe(
                    [
                      'Unified git diff for a single file.',
                      'MUST include `diff --git`, `--- a/...`, `+++ b/...`, and complete hunk headers.',
                      'CRITICAL: Every hunk header must have line numbers in format: @@ -oldStart,oldLines +newStart,newLines @@',
                      'Do NOT use @@ without the line range numbers - this will cause parsing failures.',
                      'Include 3-5 context lines before and after changes for proper patch application.',
                      'Use `/dev/null` with `---` for new files, or with `+++` for deleted files.',
                      '',
                      'Example modify patch:',
                      '```diff',
                      'diff --git a/charts/bcs-mcp-server/templates/service-api.yaml b/charts/bcs-mcp-server/templates/service-api.yaml',
                      '--- a/charts/bcs-mcp-server/templates/service-api.yaml',
                      '+++ b/charts/bcs-mcp-server/templates/service-api.yaml',
                      '@@ -4,7 +4,7 @@ spec:',
                      ' spec:',
                      '   type: {{ .Values.service.type }}',
                      '   ports:',
                      '-    - port: 8080',
                      '+    - port: 9090',
                      '     targetPort: deployment-port',
                      '     protocol: TCP',
                      '     name: http',
                      '```',
                    ].join('\n'),
                  ),
                search: z
                  .string()
                  .optional()
                  .describe(
                    [
                      'Alternative to patch: Exact text to search for in the file.',
                      'Must be used with "replace" and "path" fields.',
                      'The server will fetch the file, perform the replacement, and generate the patch automatically.',
                      'This is MUCH EASIER than creating unified diffs manually - no line counting needed!',
                      '',
                      'Example:',
                      '"search": "return axios.post(apiUrl, payload, requestConfig);"',
                      '"replace": "return axios.post(apiUrl, payload, requestConfig).then(r => { /* process */ return r; });"',
                    ].join('\n'),
                  ),
                replace: z
                  .string()
                  .optional()
                  .describe(
                    'Alternative to patch: Exact text to replace the "search" string with. Must be used together with "search" and "path".',
                  ),
              })
              .refine(
                (data) => {
                  const hasPatch = !!data.patch;
                  const hasSearchReplace = !!data.search && !!data.replace;
                  return hasPatch || hasSearchReplace;
                },
                {
                  message:
                    'Either "patch" or both "search" and "replace" must be provided',
                },
              ),
          )
          .describe(
            'List of file changes as either unified git diffs OR search/replace pairs',
          ),
      })
      .describe(
        [
          'Create a commit on an existing branch using file changes.',
          '- Provide plain branch names (no "refs/heads/").',
          '',
          '**RECOMMENDED: Use search/replace format (easier, no line counting needed!)**',
          '',
          'Option 1 - Search/Replace (Easiest):',
          '```json',
          '{',
          '  "changes": [{',
          '    "path": "src/file.ts",',
          '    "search": "old code here",',
          '    "replace": "new code here"',
          '  }]',
          '}',
          '```',
          '',
          'Option 2 - Unified Diff (Advanced):',
          '- Requires complete hunk headers: @@ -oldStart,oldLines +newStart,newLines @@',
          '- Include 3-5 context lines before/after changes',
          '- For deletions: --- a/file, +++ /dev/null',
          '- For additions: --- /dev/null, +++ b/file',
        ].join('\n'),
      );
  • MCP tool registration definition for 'create_commit', including name, detailed description with usage examples, and JSON schema derived from Zod schema.
    {
      name: 'create_commit',
      description: [
        'Create a commit on an existing branch using file changes.',
        '- Provide plain branch names (no "refs/heads/").',
        '- ⚠️ Each file path may appear only once per commit request—combine all edits to a file into a single change entry.',
        '- Prefer multiple commits when you have sparse or unrelated edits; smaller focused commits keep review context clear.',
        '',
        '🎯 RECOMMENDED: Use the SEARCH/REPLACE format (much easier, no line counting!).',
        '',
        '**Option 1: SEARCH/REPLACE format (EASIEST)**',
        'Simply provide the exact text to find and replace:',
        '```json',
        '{',
        '  "changes": [{',
        '    "path": "src/api/services/function-call.ts",',
        '    "search": "return axios.post(apiUrl, payload, requestConfig);",',
        '    "replace": "return axios.post(apiUrl, payload, requestConfig).then(r => { processResponse(r); return r; });"',
        '  }]',
        '}',
        '```',
        'The server fetches the file, performs the replacement, and generates the diff automatically.',
        'No line counting, no hunk headers, no context lines needed!',
        '',
        '**Option 2: UNIFIED DIFF format (Advanced)**',
        'If you prefer full control, provide complete unified diffs:',
        '- Each patch MUST have complete hunk headers: @@ -oldStart,oldLines +newStart,newLines @@',
        '- CRITICAL: Every @@ marker MUST include line numbers. Do NOT use @@ without line ranges.',
        '- Include 3-5 context lines before and after changes.',
        '- For deletions: `--- a/filepath` and `+++ /dev/null`',
        '- For additions: `--- /dev/null` and `+++ b/filepath`',
        '',
        'Example unified diff:',
        '```json',
        '{',
        '  "changes": [{',
        '    "patch": "diff --git a/file.yaml b/file.yaml\\n--- a/file.yaml\\n+++ b/file.yaml\\n@@ -4,7 +4,7 @@ spec:\\n spec:\\n   type: ClusterIP\\n   ports:\\n-    - port: 8080\\n+    - port: 9090\\n       targetPort: http\\n"',
        '  }]',
        '}',
        '```',
      ].join('\n'),
      inputSchema: zodToJsonSchema(CreateCommitSchema),
    },
  • Dispatcher handler in repositories index that routes 'create_commit' tool calls: parses arguments with schema and invokes the core createCommit function.
    case 'create_commit': {
      const args = CreateCommitSchema.parse(request.params.arguments);
      await createCommit(connection, {
        ...args,
        projectId: args.projectId ?? defaultProject,
      });
      return {
        content: [{ type: 'text', text: 'Commit created successfully' }],
      };
    }
  • TypeScript interfaces defining FileChange and CreateCommitOptions used by the handler and schema.
    export interface FileChange {
      /**
       * Optional path hint for the change. If omitted, the path from the diff
       * header will be used.
       */
      path?: string;
      /** Unified diff patch representing the change */
      patch?: string;
      /**
       * Alternative to patch: exact string to search for in the file.
       * Must be used together with 'replace'. The server will generate the diff.
       */
      search?: string;
      /**
       * Alternative to patch: exact string to replace 'search' with.
       * Must be used together with 'search'. The server will generate the diff.
       */
      replace?: string;
    }
    
    /**
     * Options for creating a commit with multiple file changes
     */
    export interface CreateCommitOptions {
      projectId: string;
      repositoryId: string;
      branchName: string;
      commitMessage: string;
      changes: FileChange[];
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively explains key behaviors: the tool creates commits (implying mutation), requires specific input formats (search/replace or unified diff), enforces constraints (e.g., 'Each file path may appear only once per commit request'), and recommends best practices (e.g., smaller focused commits). However, it lacks details on error handling, permissions, or rate limits, which are important for a mutation tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with clear sections (purpose, guidelines, format options) and uses bullet points and code examples effectively. It is appropriately sized for a complex tool but could be slightly more concise by reducing repetition of format details already in the schema. Every sentence adds value, such as explaining constraints and recommendations.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (6 parameters, mutation operation, no annotations, no output schema), the description is largely complete. It covers purpose, usage, input formats, and constraints. However, it lacks information on output (e.g., what the commit returns) and error cases, which would enhance completeness for a mutation tool with no structured output schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage, so the baseline is 3. The description adds significant value by explaining the semantics of the 'changes' parameter in detail, including two format options (search/replace and unified diff) with examples, recommendations, and constraints. This goes beyond the schema's technical descriptions to clarify practical usage, though it doesn't add meaning to other parameters like projectId or organizationId.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose: 'Create a commit on an existing branch using file changes.' It specifies the verb ('Create a commit'), resource ('on an existing branch'), and mechanism ('using file changes'). This distinguishes it from sibling tools like create_branch (creates branches) or list_commits (reads commits).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit guidance on when to use this tool vs alternatives. It recommends using search/replace format for ease over unified diff format, advises on structuring commits (e.g., 'Prefer multiple commits when you have sparse or unrelated edits'), and distinguishes from sibling tools by focusing on commit creation rather than operations like get_file_content or create_pull_request.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Tiberriver256/mcp-server-azure-devops'

If you have feedback or need assistance with the MCP directory API, please join our Discord server