Skip to main content
Glama

Git MCP Server

git-set-working-dir.tool.ts12 kB
/** * @fileoverview Git set working directory tool - manage session working directory * @module mcp-server/tools/definitions/git-set-working-dir */ import { z } from 'zod'; import { withToolAuth } from '@/mcp-server/transports/auth/lib/withAuth.js'; import { createJsonFormatter, type VerbosityLevel, } from '../utils/json-response-formatter.js'; import type { ToolDefinition } from '../utils/toolDefinition.js'; import { createToolHandler, type ToolLogicDependencies, } from '../utils/toolHandlerFactory.js'; const TOOL_NAME = 'git_set_working_dir'; const TOOL_TITLE = 'Git Set Working Directory'; const TOOL_DESCRIPTION = 'Set the session working directory for all git operations. This allows subsequent git commands to omit the path parameter and use this directory as the default.'; const InputSchema = z.object({ path: z .string() .min(1) .describe( 'Absolute path to the git repository to use as the working directory.', ), validateGitRepo: z .boolean() .default(true) .describe('Validate that the path is a Git repository.'), initializeIfNotPresent: z .boolean() .default(false) .describe("If not a Git repository, initialize it with 'git init'."), includeContext: z .boolean() .default(true) .describe( 'Include repository context (status, branches, remotes, recent commits) in the response. Provides immediate understanding of repository state.', ), }); const OutputSchema = z.object({ success: z.boolean().describe('Indicates if the operation was successful.'), path: z.string().describe('The working directory that was set.'), message: z.string().describe('Confirmation message.'), repositoryContext: z .object({ status: z .object({ branch: z .string() .nullable() .describe('Current branch name (null if detached HEAD).'), isClean: z.boolean().describe('True if working directory is clean.'), stagedCount: z .number() .int() .describe('Number of staged files ready for commit.'), unstagedCount: z .number() .int() .describe('Number of files with unstaged changes.'), untrackedCount: z .number() .int() .describe('Number of untracked files.'), conflictsCount: z .number() .int() .describe('Number of files with merge conflicts.'), }) .describe('Current repository working tree status.'), branches: z .object({ current: z .string() .nullable() .describe('Current branch name (null if detached HEAD).'), totalLocal: z .number() .int() .describe('Total number of local branches.'), totalRemote: z .number() .int() .describe('Total number of remote-tracking branches.'), upstream: z .string() .optional() .describe( 'Upstream branch name if current branch is tracking one.', ), ahead: z .number() .int() .optional() .describe('Commits ahead of upstream (if tracking).'), behind: z .number() .int() .optional() .describe('Commits behind upstream (if tracking).'), }) .describe('Branch information and tracking status.'), remotes: z .array( z.object({ name: z.string().describe('Remote name.'), fetchUrl: z.string().describe('Fetch URL.'), pushUrl: z.string().describe('Push URL (may differ from fetch).'), }), ) .describe('Configured remote repositories.'), recentCommits: z .array( z.object({ hash: z.string().describe('Commit hash (short form).'), author: z.string().describe('Commit author name.'), date: z.string().describe('Commit date (ISO 8601 format).'), message: z.string().describe('Commit message (first line).'), }), ) .describe('Recent commits (up to 5 most recent).'), }) .optional() .describe( 'Rich repository context including status, branches, remotes, and recent history. Only included when includeContext is true.', ), }); type ToolInput = z.infer<typeof InputSchema>; type ToolOutput = z.infer<typeof OutputSchema>; /** * Gather rich repository context including status, branches, remotes, and recent commits. * This provides LLMs with immediate understanding of repository state. * * Failures in context gathering are logged but don't fail the operation - context is * nice-to-have enrichment, not critical for setting the working directory. */ async function gatherRepositoryContext( targetPath: string, dependencies: ToolLogicDependencies, ): Promise<ToolOutput['repositoryContext']> { const { provider, appContext } = dependencies; const tenantId = appContext.tenantId || 'default-tenant'; const context = { workingDirectory: targetPath, requestContext: appContext, tenantId, }; try { // Gather all context in parallel for efficiency const [statusResult, branchesResult, remotesResult, logResult] = await Promise.allSettled([ provider.status({ includeUntracked: true }, context), provider.branch({ mode: 'list', remote: true }, context), provider.remote({ mode: 'list' }, context), provider.log({ maxCount: 5 }, context), ]); // Process status const status = statusResult.status === 'fulfilled' ? { branch: statusResult.value.currentBranch, isClean: statusResult.value.isClean, stagedCount: (statusResult.value.stagedChanges.added?.length || 0) + (statusResult.value.stagedChanges.modified?.length || 0) + (statusResult.value.stagedChanges.deleted?.length || 0) + (statusResult.value.stagedChanges.renamed?.length || 0) + (statusResult.value.stagedChanges.copied?.length || 0), unstagedCount: (statusResult.value.unstagedChanges.added?.length || 0) + (statusResult.value.unstagedChanges.modified?.length || 0) + (statusResult.value.unstagedChanges.deleted?.length || 0), untrackedCount: statusResult.value.untrackedFiles.length, conflictsCount: statusResult.value.conflictedFiles.length, } : { branch: null, isClean: false, stagedCount: 0, unstagedCount: 0, untrackedCount: 0, conflictsCount: 0, }; // Process branches const branches: NonNullable<ToolOutput['repositoryContext']>['branches'] = branchesResult.status === 'fulfilled' && branchesResult.value.mode === 'list' ? (() => { const branchList = branchesResult.value.branches; const currentBranch = branchList.find((b) => b.current); const localBranches = branchList.filter( (b) => !b.name.startsWith('remotes/'), ); const remoteBranches = branchList.filter((b) => b.name.startsWith('remotes/'), ); return { current: currentBranch?.name || null, totalLocal: localBranches.length, totalRemote: remoteBranches.length, upstream: currentBranch?.upstream, ahead: currentBranch?.ahead, behind: currentBranch?.behind, }; })() : { current: null, totalLocal: 0, totalRemote: 0, }; // Process remotes const remotes: NonNullable<ToolOutput['repositoryContext']>['remotes'] = remotesResult.status === 'fulfilled' && remotesResult.value.mode === 'list' ? remotesResult.value.remotes || [] : []; // Process recent commits const recentCommits: NonNullable< ToolOutput['repositoryContext'] >['recentCommits'] = logResult.status === 'fulfilled' ? logResult.value.commits.map((commit) => ({ hash: commit.shortHash, author: commit.author, date: new Date(commit.timestamp * 1000).toISOString(), message: commit.subject, })) : []; return { status, branches, remotes, recentCommits, }; } catch (error) { // Log error but return undefined - context gathering is optional const { logger } = await import('@/utils/index.js'); logger.debug('Failed to gather repository context', { ...appContext, error: error instanceof Error ? error.message : String(error), targetPath, }); return undefined; } } async function gitSetWorkingDirLogic( input: ToolInput, dependencies: ToolLogicDependencies, ): Promise<ToolOutput> { const { provider, storage, appContext } = dependencies; // Graceful degradation for tenantId const tenantId = appContext.tenantId || 'default-tenant'; // Validate git repository if requested (using provider interface instead of direct CLI import) if (input.validateGitRepo) { try { await provider.validateRepository(input.path, { workingDirectory: input.path, requestContext: appContext, tenantId, }); } catch (error) { // If validation fails and initializeIfNotPresent is true, initialize the repo if (input.initializeIfNotPresent) { await provider.init( { path: input.path, initialBranch: 'main', bare: false, }, { workingDirectory: input.path, requestContext: appContext, tenantId, }, ); } else { // Re-throw validation error if initializeIfNotPresent is false throw error; } } } // Store the working directory in session storage const storageKey = `session:workingDir:${tenantId}`; await storage.set(storageKey, input.path, appContext); // Gather repository context if requested const repositoryContext = input.includeContext ? await gatherRepositoryContext(input.path, dependencies) : undefined; return { success: true, path: input.path, message: `Working directory set to: ${input.path}`, repositoryContext, }; } /** * Filter git_set_working_dir output based on verbosity level. * * Verbosity levels: * - minimal: Success and path only (no context or message) * - standard: Success, path, message, and full repository context (RECOMMENDED for LLM understanding) * - full: Complete output (same as standard - all fields included) */ function filterGitSetWorkingDirOutput( result: ToolOutput, level: VerbosityLevel, ): Partial<ToolOutput> { // minimal: Essential info only - no context if (level === 'minimal') { return { success: result.success, path: result.path, }; } // standard & full: Complete output including repository context // Repository context is critical for LLM understanding - don't filter it return result; } // Create JSON response formatter with verbosity filtering const responseFormatter = createJsonFormatter<ToolOutput>({ filter: filterGitSetWorkingDirOutput, }); export const gitSetWorkingDirTool: ToolDefinition< typeof InputSchema, typeof OutputSchema > = { name: TOOL_NAME, title: TOOL_TITLE, description: TOOL_DESCRIPTION, inputSchema: InputSchema, outputSchema: OutputSchema, annotations: { readOnlyHint: false }, logic: withToolAuth( ['tool:git:write'], createToolHandler(gitSetWorkingDirLogic, { skipPathResolution: true }), ), responseFormatter, };

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/cyanheads/git-mcp-server'

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