Version: 2.15.0 Target Project: git-mcp-server Last Updated: 2026-04-28
This document defines the operational rules for contributing to this codebase. Follow it exactly.
Note on File Synchronization:
AGENTS.mdis a symlink toCLAUDE.md. Edit onlyCLAUDE.md—AGENTS.mdwill reflect changes automatically.
-
The Logic Throws, The Handler Catches
- Tools: Implement pure, stateless business logic inside tool logic functions. No
try/catchblocks. - Resources: Same rule — pure read logic, no
try/catch. - On Failure: Throw
new McpError(...)with the appropriateJsonRpcErrorCodeand context. - Framework's Job:
createMcpToolHandlerwraps tool logic: createsRequestContext, measures execution viameasureToolExecution, formats the response, catches errors.createToolHandlerwraps git-specific logic: resolves DI dependencies and working directory before calling your pure logic.- Resource handlers (
resourceHandlerFactory) validate params, invoke logic, applyresponseFormatter, and catch errors.
- Tools: Implement pure, stateless business logic inside tool logic functions. No
-
Full-Stack Observability
- OpenTelemetry is preconfigured. Logs and errors are automatically correlated to traces.
measureToolExecutionautomatically records duration, success, payload sizes, and error codes for every tool call.- Do not add custom spans in tool/resource logic. The framework handles instrumentation.
-
Structured, Traceable Operations
- Tool logic receives dependencies via
ToolLogicDependencies(which includesappContextandsdkContext). appContext(RequestContext): Internal logging/tracing context withrequestId,sessionId,tenantId,traceId.sdkContext(SdkContext): MCP SDK protocol capabilities —signal,sendNotification,sendRequest,authInfo.- Pass
appContextthrough your internal call stack. Use the globalloggerwithappContextin every log call.
- Tool logic receives dependencies via
-
Decoupled Storage
- Never directly access persistence backends from tool/resource logic.
- Use
StorageService(injected via DI) for session state (working directory persistence). - Git operations execute via the
IGitProviderinterface, not direct CLI calls.
-
Graceful Degradation in Development
- When
tenantIdis missing, default to permissive behavior:const tenantId = appContext.tenantId || 'default-tenant'; - Auth/scope checks default to allowed when auth is disabled.
- Production environments with auth enabled provide real
tenantIdfrom JWT claims automatically.
- When
| Directory | Purpose |
|---|---|
src/mcp-server/tools/definitions/ |
MCP Tool definitions. Named git-[operation].tool.ts. |
src/mcp-server/tools/utils/ |
Shared tool utilities: toolDefinition.ts, toolHandlerFactory.ts, git-validators.ts, json-response-formatter.ts. |
src/mcp-server/tools/schemas/ |
Shared Zod schemas: PathSchema, CommitRefSchema, BranchNameSchema, etc. |
src/mcp-server/resources/definitions/ |
MCP Resource definitions. Primary: git-working-directory.resource.ts. |
src/mcp-server/resources/utils/ |
Shared resource utilities: ResourceDefinition and handler factory. |
src/mcp-server/prompts/definitions/ |
MCP Prompt definitions (e.g., git-wrapup.prompt.ts). |
src/mcp-server/transports/ |
Transport implementations: http/ (Hono + Streamable HTTP), stdio/, auth/ (JWT/OAuth strategies). |
src/services/git/ |
Git service: core/ (interfaces, factory), providers/cli/ (CLI implementation with domain-organized operations). |
src/storage/ |
Storage abstractions and providers (in-memory, filesystem, supabase, cloudflare). |
src/container/ |
Dependency injection (tsyringe). Service registration and tokens. |
src/utils/ |
Global utilities: internal/ (logger, requestContext, ErrorHandler, performance), security/ (sanitization), telemetry/, metrics/. |
tests/ |
Unit/integration tests mirroring src/ structure. |
- Place new tools in
src/mcp-server/tools/definitions/. - Name files
git-[operation].tool.ts(e.g.,git-commit.tool.ts,git-status.tool.ts). - Use existing tools as reference (e.g.,
git-status.tool.ts).
Export a single const named [toolName]Tool of type ToolDefinition with:
name: Programmatic tool name,snake_casewithgit_prefix (e.g.,git_status,git_commit).title(optional): Human-readable title (e.g.,'Git Status').description: Clear, LLM-facing description.inputSchema: Az.object({ ... }). Every field must have.describe(). Use shared schemas fromschemas/common.ts(PathSchema,CommitRefSchema, etc.).outputSchema: Az.object({ ... })describing the successful output structure.annotations(optional): UI/behavior hints (readOnlyHint,openWorldHint, etc.).logic: Wrapped withcreateToolHandler(see below).responseFormatter(optional): UsecreateJsonFormatterfor consistent output.
Tool logic uses a two-tier handler pattern:
createToolHandlerresolves DI dependencies (StorageService, GitProviderFactory) once via lazy closure, resolves the working directory frominput.path, and passes everything to your pure logic function asToolLogicDependencies.createMcpToolHandler(framework-level) wraps everything for the MCP SDK: context creation, performance measurement, error handling, response formatting.
Your logic function receives (input, deps: ToolLogicDependencies):
interface ToolLogicDependencies {
provider: IGitProvider; // Git provider, already resolved
storage: StorageService; // Storage service, already resolved
appContext: RequestContext; // Request context for logging/tracing
sdkContext: SdkContext; // MCP SDK context (signal, sendNotification, etc.)
targetPath: string; // Resolved working directory (from input.path)
}For tools that don't need path resolution (e.g., git_clone, git_set_working_dir), pass { skipPathResolution: true }:
logic: withToolAuth(['tool:git:write'],
createToolHandler(myLogic, { skipPathResolution: true })
),Wrap logic with withToolAuth:
logic: withToolAuth(['tool:git:read'], createToolHandler(gitStatusLogic)),Scopes: tool:git:read for read-only, tool:git:write for mutations.
Add your tool to src/mcp-server/tools/definitions/index.ts in allToolDefinitions.
/**
* @fileoverview Git status tool - show working tree status
* @module mcp-server/tools/definitions/git-status
*/
import { z } from 'zod';
import type { ToolDefinition } from '../utils/toolDefinition.js';
import { withToolAuth } from '@/mcp-server/transports/auth/lib/withAuth.js';
import { PathSchema } from '../schemas/common.js';
import {
createToolHandler,
type ToolLogicDependencies,
} from '../utils/toolHandlerFactory.js';
import {
createJsonFormatter,
type VerbosityLevel,
} from '../utils/json-response-formatter.js';
const TOOL_NAME = 'git_status';
const TOOL_TITLE = 'Git Status';
const TOOL_DESCRIPTION =
'Show the working tree status including staged, unstaged, and untracked files.';
const InputSchema = z.object({
path: PathSchema,
includeUntracked: z
.boolean()
.default(true)
.describe('Include untracked files in the output.'),
});
const OutputSchema = z.object({
success: z.boolean().describe('Indicates if the operation was successful.'),
currentBranch: z.string().nullable().describe('Current branch name.'),
isClean: z.boolean().describe('True if working directory is clean.'),
stagedChanges: z
.object({
added: z
.array(z.string())
.optional()
.describe('Files added to the index.'),
modified: z
.array(z.string())
.optional()
.describe('Files modified and staged.'),
deleted: z
.array(z.string())
.optional()
.describe('Files deleted and staged.'),
})
.describe('Changes staged for the next commit.'),
unstagedChanges: z
.object({
modified: z
.array(z.string())
.optional()
.describe('Files modified but not staged.'),
deleted: z
.array(z.string())
.optional()
.describe('Files deleted but not staged.'),
})
.describe('Changes not yet staged.'),
untrackedFiles: z.array(z.string()).describe('Untracked files.'),
conflictedFiles: z.array(z.string()).describe('Files with merge conflicts.'),
});
type ToolInput = z.infer<typeof InputSchema>;
type ToolOutput = z.infer<typeof OutputSchema>;
// Pure business logic — receives pre-resolved dependencies
async function gitStatusLogic(
input: ToolInput,
{ provider, targetPath, appContext }: ToolLogicDependencies,
): Promise<ToolOutput> {
const result = await provider.status(
{ includeUntracked: input.includeUntracked },
{
workingDirectory: targetPath,
requestContext: appContext,
tenantId: appContext.tenantId || 'default-tenant',
},
);
return {
success: true,
currentBranch: result.currentBranch,
isClean: result.isClean,
stagedChanges: result.stagedChanges,
unstagedChanges: result.unstagedChanges,
untrackedFiles: result.untrackedFiles,
conflictedFiles: result.conflictedFiles,
};
}
// Verbosity filter for response formatting
function filterGitStatusOutput(
result: ToolOutput,
level: VerbosityLevel,
): Partial<ToolOutput> {
if (level === 'minimal') {
return {
success: result.success,
currentBranch: result.currentBranch,
isClean: result.isClean,
};
}
return result; // standard & full return everything
}
const responseFormatter = createJsonFormatter<ToolOutput>({
filter: filterGitStatusOutput,
});
export const gitStatusTool: ToolDefinition<
typeof InputSchema,
typeof OutputSchema
> = {
name: TOOL_NAME,
title: TOOL_TITLE,
description: TOOL_DESCRIPTION,
inputSchema: InputSchema,
outputSchema: OutputSchema,
annotations: { readOnlyHint: true },
logic: withToolAuth(['tool:git:read'], createToolHandler(gitStatusLogic)),
responseFormatter,
};All tools use createJsonFormatter for consistent, machine-readable JSON output with verbosity control (minimal, standard, full).
Basic usage:
import {
createJsonFormatter,
shouldInclude,
type VerbosityLevel,
} from '../utils/json-response-formatter.js';
function filterOutput(
result: ToolOutput,
level: VerbosityLevel,
): Partial<ToolOutput> {
return {
success: result.success,
commitHash: result.commitHash,
...(shouldInclude(level, 'standard') && { files: result.files }),
...(shouldInclude(level, 'full') && {
detailedStatus: result.detailedStatus,
}),
};
}
const responseFormatter = createJsonFormatter<ToolOutput>({
filter: filterOutput,
});Rules:
- Include or omit entire fields based on verbosity — never truncate arrays.
- Return complete arrays when included (LLMs need full context).
- For simple tools with minimal output, omit the filter entirely:
createJsonFormatter<ToolOutput>().
Additional utilities: filterByVerbosity(), mergeFilters(), createFieldMapper(), createConditionalFilter().
Tools MUST use the IGitProvider interface for all git operations. Direct git command execution is forbidden in the tool layer.
┌─────────────────────────────────────────────────┐
│ Tool Layer (src/mcp-server/tools/) │
│ - Input validation (Zod schemas) │
│ - Path resolution (via createToolHandler) │
│ - Output formatting for LLM │
│ - Pure validators (no git execution) │
└─────────────────────┬───────────────────────────┘
│ IGitProvider interface
▼
┌─────────────────────────────────────────────────┐
│ Service Layer (src/services/git/) │
│ - Git command execution │
│ - Git-specific validators │
│ - Output parsing & error transformation │
└─────────────────────────────────────────────────┘
| Validator Type | Location | Reason |
|---|---|---|
| Path sanitization | Tool layer (git-validators.ts) |
Security, no git execution |
| Session directory resolution | Tool layer (git-validators.ts) |
Uses StorageService |
| Protected branch checks | Tool layer (git-validators.ts) |
Pure logic |
| File path / commit message validation | Tool layer (git-validators.ts) |
Pure validation |
| Git repository validation | Service layer (cli/utils/git-validators.ts) |
Executes git rev-parse |
| Branch existence check | Service layer (cli/utils/git-validators.ts) |
Executes git rev-parse --verify |
| Clean working dir check | Service layer (cli/utils/git-validators.ts) |
Executes git status --porcelain |
| Remote existence check | Service layer (cli/utils/git-validators.ts) |
Executes git remote get-url |
Handled automatically by createToolHandler. When input.path is '.', it loads from session storage using key session:workingDir:{tenantId}. When it's an absolute path, it's used directly. All paths are sanitized to prevent directory traversal.
The git service uses a provider-based architecture with the CLI provider as the current implementation.
src/services/git/
├── core/
│ ├── IGitProvider.ts # Provider interface contract
│ ├── BaseGitProvider.ts # Shared provider functionality
│ └── GitProviderFactory.ts # Provider selection and caching
├── providers/cli/
│ ├── CliGitProvider.ts # Main provider class
│ ├── operations/ # Operations organized by domain
│ │ ├── core/ # init, clone, status, clean
│ │ ├── staging/ # add, reset
│ │ ├── commits/ # commit, log, show, diff
│ │ ├── branches/ # branch, checkout, merge, rebase, cherry-pick
│ │ ├── remotes/ # remote, fetch, push, pull
│ │ ├── tags/ # tag
│ │ ├── stash/ # stash
│ │ ├── worktree/ # worktree
│ │ ├── history/ # blame, reflog
│ │ └── index.ts # Single barrel export
│ └── utils/ # CLI-specific utilities and validators
├── types.ts # Shared git types and DTOs
└── index.ts # Public API
- Each file handles exactly one operation (one function per file).
- All operations are stateless async functions that throw
McpErroron failure. - Operations receive an
execGitfunction for executing git commands, keeping them testable. - Single barrel export at
operations/index.ts(no nested barrels).
All providers implement: init, clone, status, clean, add, commit, log, show, diff, branch, checkout, merge, rebase, cherryPick, remote, fetch, push, pull, tag, stash, worktree, reset, blame, reflog.
Each provider declares capabilities via GitProviderCapabilities, allowing consumers to check feature support.
- CLI (
GitProviderType.CLI): Full feature set, local-only (default, current) - Isomorphic (
GitProviderType.ISOMORPHIC): Edge-compatible (planned)
Resources follow the same pattern as tools with a declarative ResourceDefinition.
- Place in
src/mcp-server/resources/definitions/as[resource-name].resource.ts. - Export a
constof typeResourceDefinitionwith:name,description,uriTemplate,paramsSchema,logic, and optionalresponseFormatter. - Wrap logic with
withResourceAuth(['resource:git:read'], ...). - Register in
src/mcp-server/resources/definitions/index.tsviaallResourceDefinitions. - Logic is pure (no
try/catch) — throwMcpErroron failure.
| Token | Purpose |
|---|---|
StorageService |
Session state (working directory persistence) |
GitProviderFactory |
Git provider selection and caching |
Logger |
Pino-backed structured logging |
AppConfig |
Validated environment configuration |
RateLimiterService |
Optional rate limiting for HTTP transport |
CreateMcpServerInstance |
Factory resolved by TransportManager |
TransportManagerToken |
Manages stdio/HTTP transport lifecycle |
logger— Global Pino logger instancerequestContextService— AsyncLocalStorage-based context propagationErrorHandler.tryCatch— For services/infrastructure (NOT in tool/resource logic)sanitization— Critical for path validation and directory traversal preventionmeasureToolExecution— Performance measurement (used by handlers automatically)
| Module | Key Exports |
|---|---|
internal/ |
logger, requestContextService, ErrorHandler, performance (measureToolExecution) |
security/ |
sanitization (path/input validation), rateLimiter, idGenerator |
telemetry/ |
OpenTelemetry instrumentation |
- Modes:
MCP_AUTH_MODE='none'|'jwt'|'oauth' - JWT mode: Uses
MCP_AUTH_SECRET_KEY. In dev without the secret, verification is bypassed. - OAuth mode: Verifies tokens via remote JWKS. Requires
OAUTH_ISSUER_URLandOAUTH_AUDIENCE. - Extracted claims:
clientId,scopes,subject,tenantId(from'tid'claim). - Scope enforcement: Always wrap logic with
withToolAuthorwithResourceAuth. Defaults to allowed when auth is disabled.
No HTTP-based auth. Authorization handled by the host application.
GET /healthz— Unprotected health checkGET /.well-known/oauth-protected-resource— RFC 9728 protected resource metadata (unprotected, for discovery)GET /mcp— Unprotected server identity and config summaryPOST /mcp— JSON-RPC transport (protected when auth enabled)DELETE /mcp— Session termination (MCP Spec 2025-06-18)- CORS enabled via
MCP_ALLOWED_ORIGINSor'*'fallback
- Configures
RequestContextglobal settings. - Creates
McpServerwith capabilities:logging,resources(listChanged),tools(listChanged),prompts(listChanged). - Registers all capabilities via DI-managed registries:
ToolRegistry,ResourceRegistry,PromptRegistry.
- Resolves
CreateMcpServerInstanceto get a configuredMcpServer. - Based on
MCP_TRANSPORT_TYPE, starts the appropriate transport (httporstdio). - Handles graceful startup and shutdown.
worker.tsadapts the server for Cloudflare Workers.- Git CLI operations require local filesystem access — edge deployment is experimental.
- JSDoc: Every file:
@fileoverviewand@module. Document exported APIs. - Validation: All inputs validated via Zod. Every schema field must have
.describe(). - Logging: Always include
RequestContext. Uselogger.debug/info/warning/errorappropriately. - Error Handling: Logic throws
McpError; handlers catch. UseErrorHandler.tryCatchin services only. - Secrets: Access via
src/config/index.ts. Never hard-code.
- Path Sanitization: All paths MUST use
sanitizationutilities to prevent directory traversal. - Command Injection: Git command arguments must be validated — never from unsanitized input.
- Destructive Operations:
git reset --hard/--merge/--keep,git clean -fdrequire explicit confirmation flags.
All configuration validated via Zod in src/config/index.ts. Derives serviceName and version from package.json if not provided.
| Category | Variables |
|---|---|
| Transport | MCP_TRANSPORT_TYPE (stdio/http), MCP_HTTP_PORT, MCP_HTTP_HOST, MCP_HTTP_PATH |
| Auth | MCP_AUTH_MODE (none/jwt/oauth), MCP_AUTH_SECRET_KEY, OAUTH_ISSUER_URL, OAUTH_AUDIENCE, OAUTH_JWKS_URI |
| Storage | STORAGE_PROVIDER_TYPE (in-memory/filesystem/supabase/cloudflare-r2/cloudflare-kv), STORAGE_FILESYSTEM_PATH |
| Git | GIT_PROVIDER (auto/cli/isomorphic), GIT_SIGN_COMMITS, GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, GIT_BASE_DIR, GIT_MAX_COMMAND_TIMEOUT_MS, GIT_MAX_BUFFER_SIZE_MB, GIT_WRAPUP_INSTRUCTIONS_PATH |
| Telemetry | OTEL_ENABLED, OTEL_SERVICE_NAME, OTEL_SERVICE_VERSION, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT |
| Command | Purpose |
|---|---|
bun rebuild |
Clean and rebuild; clears logs. Run after dependency changes. |
bun devcheck |
Lint, format, typecheck, security audit. Flags: --no-fix, --no-lint, --no-audit. |
bun test |
Run unit/integration tests. |
bun run dev:stdio / dev:http |
Development mode. |
bun run start:stdio / start:http |
Production mode (after build). |
bun run build:worker |
Build Cloudflare Worker bundle. |
StorageServicerequirescontext.tenantId— throwsMcpErrorif missing.- In HTTP + auth mode,
tenantIdis automatically extracted from JWT'tid'claim. - In development (STDIO/no auth), use graceful degradation:
appContext.tenantId || 'default-tenant'. - The
createToolHandlerfactory handles this pattern for you —resolveWorkingDirectoryapplies it internally.
- Tool/resource logic in
*.tool.tsor*.resource.ts, pure (notry/catch). - Throws
McpErrorfor failures. - Uses
createToolHandlerfor dependency injection and path resolution. - Wrapped with
withToolAuthorwithResourceAuth. - Uses
loggerwithappContextfor logging. - All file paths validated via
sanitizationutilities. - Git command arguments validated (no command injection).
- Registered in
index.tsbarrel file. - Tests added/updated (
bun test). -
bun run devcheckpasses. - Smoke-tested local transports (
dev:stdio/dev:http).