@nvhien20vn/git-mcp-server
Version:
303 lines (302 loc) • 19.3 kB
JavaScript
/**
* Main entry point for the MCP (Model Context Protocol) server.
* This file orchestrates the server's lifecycle:
* 1. Initializes the core McpServer instance with its identity and capabilities.
* 2. Registers available resources and tools, making them discoverable and usable by clients.
* 3. Selects and starts the appropriate communication transport (stdio or Streamable HTTP)
* based on configuration.
* 4. Handles top-level error management during startup.
*
* MCP Specification References:
* - Lifecycle: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/lifecycle.mdx
* - Overview (Capabilities): https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/index.mdx
* - Transports: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
// Import validated configuration and environment details.
import { config, environment } from '../config/index.js';
// Import core utilities: ErrorHandler, logger, requestContextService.
import { ErrorHandler, logger, requestContextService } from '../utils/index.js'; // Added RequestContext
// Import registration AND state initialization functions for ALL Git tools (alphabetized)
import { initializeGitAddStateAccessors, registerGitAddTool } from './tools/gitAdd/index.js';
import { initializeGitBranchStateAccessors, registerGitBranchTool } from './tools/gitBranch/index.js';
import { initializeGitCheckoutStateAccessors, registerGitCheckoutTool } from './tools/gitCheckout/index.js';
import { initializeGitCherryPickStateAccessors, registerGitCherryPickTool } from './tools/gitCherryPick/index.js';
import { initializeGitCleanStateAccessors, registerGitCleanTool } from './tools/gitClean/index.js';
import { initializeGitClearWorkingDirStateAccessors, registerGitClearWorkingDirTool } from './tools/gitClearWorkingDir/index.js';
import { registerGitCloneTool } from './tools/gitClone/index.js'; // No initializer needed/available
import { initializeGitCommitStateAccessors, registerGitCommitTool } from './tools/gitCommit/index.js';
import { initializeGitDiffStateAccessors, registerGitDiffTool } from './tools/gitDiff/index.js';
import { initializeGitFetchStateAccessors, registerGitFetchTool } from './tools/gitFetch/index.js';
import { initializeGitInitStateAccessors, registerGitInitTool } from './tools/gitInit/index.js';
import { initializeGitLogStateAccessors, registerGitLogTool } from './tools/gitLog/index.js';
import { initializeGitMergeStateAccessors, registerGitMergeTool } from './tools/gitMerge/index.js';
import { initializeGitPullStateAccessors, registerGitPullTool } from './tools/gitPull/index.js';
import { initializeGitPushStateAccessors, registerGitPushTool } from './tools/gitPush/index.js';
import { initializeGitRebaseStateAccessors, registerGitRebaseTool } from './tools/gitRebase/index.js';
import { initializeGitRemoteStateAccessors, registerGitRemoteTool } from './tools/gitRemote/index.js';
import { initializeGitResetStateAccessors, registerGitResetTool } from './tools/gitReset/index.js';
import { initializeGitSetWorkingDirStateAccessors, registerGitSetWorkingDirTool } from './tools/gitSetWorkingDir/index.js';
import { initializeGitShowStateAccessors, registerGitShowTool } from './tools/gitShow/index.js';
import { initializeGitStashStateAccessors, registerGitStashTool } from './tools/gitStash/index.js';
import { initializeGitStatusStateAccessors, registerGitStatusTool } from './tools/gitStatus/index.js';
import { initializeGitTagStateAccessors, registerGitTagTool } from './tools/gitTag/index.js';
import { initializeGitWorktreeStateAccessors, registerGitWorktreeTool } from './tools/gitWorktree/index.js';
import { initializeGitWrapupInstructionsStateAccessors, registerGitWrapupInstructionsTool } from './tools/gitWrapupInstructions/index.js';
// Import transport setup functions AND state accessors
import { getHttpSessionWorkingDirectory, setHttpSessionWorkingDirectory, startHttpTransport } from './transports/httpTransport.js';
import { connectStdioTransport, getStdioWorkingDirectory, setStdioWorkingDirectory } from './transports/stdioTransport.js';
/**
* Creates and configures a new instance of the McpServer.
*
* This function is central to defining the server's identity and functionality
* as presented to connecting clients during the MCP initialization phase.
*
* MCP Spec Relevance:
* - Server Identity (`serverInfo`): The `name` and `version` provided here are part
* of the `ServerInformation` object returned in the `InitializeResult` message,
* allowing clients to identify the server they are connected to.
* - Capabilities Declaration: The `capabilities` object declares the features this
* server supports, enabling clients to tailor their interactions.
* - `logging: {}`: Indicates the server can receive `logging/setLevel` requests
* and may send `notifications/message` log messages (handled by the logger utility).
* - `resources: { listChanged: true }`: Signals that the server supports dynamic
* resource lists and will send `notifications/resources/list_changed` if the
* available resources change after initialization. (Currently no resources registered)
* - `tools: { listChanged: true }`: Signals support for dynamic tool lists and
* `notifications/tools/list_changed`.
* - Resource/Tool Registration: This function calls specific registration functions
* (e.g., `registerGitAdd`) which use SDK methods (`server.resource`, `server.tool`)
* to make capabilities available for discovery (`resources/list`, `tools/list`) and
* invocation (`resources/read`, `tools/call`).
*
* Design Note: This factory function is used to create server instances. For the 'stdio'
* transport, it's called once. For the 'http' transport, it's passed to `startHttpTransport`
* and called *per session* to ensure session isolation.
*
* @returns {Promise<McpServer>} A promise resolving with the configured McpServer instance.
* @throws {Error} If any resource or tool registration fails.
*/
// Removed sessionId parameter, it will be retrieved from context within tool handlers
async function createMcpServerInstance() {
const context = { operation: 'createMcpServerInstance' };
logger.info('Initializing MCP server instance', context);
// Configure the request context service (used for correlating logs/errors).
requestContextService.configure({
appName: config.mcpServerName,
appVersion: config.mcpServerVersion,
environment,
});
// Instantiate the core McpServer using the SDK.
// Provide server identity (name, version) and declare supported capabilities.
// Note: Resources capability declared, but none are registered currently.
logger.debug('Instantiating McpServer with capabilities', { ...context, serverInfo: { name: config.mcpServerName, version: config.mcpServerVersion }, capabilities: { logging: {}, resources: { listChanged: true }, tools: { listChanged: true } } });
const server = new McpServer({ name: config.mcpServerName, version: config.mcpServerVersion }, // ServerInformation part of InitializeResult
{ capabilities: { logging: {}, resources: { listChanged: true }, tools: { listChanged: true } } } // Declared capabilities
);
// --- Define Unified State Accessor Functions ---
// These functions abstract away the transport type to get/set session state.
/** Gets the session ID from the tool's execution context. */
const getSessionIdFromContext = (toolContext) => {
// The RequestContext created by the tool registration wrapper should contain the sessionId.
return toolContext?.sessionId;
};
/** Gets the working directory based on transport type and session ID. */
const getWorkingDirectory = (sessionId) => {
if (config.mcpTransportType === 'http') {
if (!sessionId) {
logger.warning('Attempted to get HTTP working directory without session ID', { ...context, caller: 'getWorkingDirectory' });
return undefined;
}
return getHttpSessionWorkingDirectory(sessionId);
}
else {
// For stdio, there's only one implicit session, ID is not needed.
return getStdioWorkingDirectory();
}
};
/** Sets the working directory based on transport type and session ID. */
const setWorkingDirectory = (sessionId, dir) => {
if (config.mcpTransportType === 'http') {
if (!sessionId) {
logger.error('Attempted to set HTTP working directory without session ID', { ...context, caller: 'setWorkingDirectory', dir });
// Optionally throw an error or just log
return;
}
setHttpSessionWorkingDirectory(sessionId, dir);
}
else {
// For stdio, set the single session's directory.
setStdioWorkingDirectory(dir);
}
};
// --- Initialize Tool State Accessors BEFORE Registration ---
// Pass the defined unified accessor functions to the initializers.
logger.debug('Initializing state accessors for tools...', context);
try {
// Call initializers for all tools that likely need state access (alphabetized).
// If an initializer doesn't exist, the import would have failed earlier (or build will fail).
initializeGitAddStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitBranchStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitCheckoutStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitCherryPickStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitCleanStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitClearWorkingDirStateAccessors(getWorkingDirectory, getSessionIdFromContext);
// initializeGitCloneStateAccessors - No initializer needed/available
initializeGitCommitStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitDiffStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitFetchStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitInitStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitLogStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitMergeStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitPullStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitPushStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitRebaseStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitRemoteStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitResetStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitSetWorkingDirStateAccessors(getWorkingDirectory, setWorkingDirectory, getSessionIdFromContext); // Special case
initializeGitShowStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitStashStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitStatusStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitTagStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitWorktreeStateAccessors(getWorkingDirectory, getSessionIdFromContext);
initializeGitWrapupInstructionsStateAccessors(getWorkingDirectory, getSessionIdFromContext); // Added this line
logger.debug('State accessors initialized successfully.', context);
}
catch (initError) {
// Catch errors specifically during initialization phase
logger.error('Failed during state accessor initialization', {
...context,
error: initError instanceof Error ? initError.message : String(initError),
stack: initError instanceof Error ? initError.stack : undefined,
});
throw initError; // Re-throw to prevent server starting incorrectly
}
try {
// Register all defined Git tools (alphabetized). These calls populate the server's
// internal registry, making them available via MCP methods like 'tools/list'.
logger.debug('Registering Git tools...', context);
await registerGitAddTool(server);
await registerGitBranchTool(server);
await registerGitCheckoutTool(server);
await registerGitCherryPickTool(server);
await registerGitCleanTool(server);
await registerGitClearWorkingDirTool(server);
await registerGitCloneTool(server);
await registerGitCommitTool(server);
await registerGitDiffTool(server);
await registerGitFetchTool(server);
await registerGitInitTool(server);
await registerGitLogTool(server);
await registerGitMergeTool(server);
await registerGitPullTool(server);
await registerGitPushTool(server);
await registerGitRebaseTool(server);
await registerGitRemoteTool(server);
await registerGitResetTool(server);
await registerGitSetWorkingDirTool(server);
await registerGitShowTool(server);
await registerGitStashTool(server);
await registerGitStatusTool(server);
await registerGitTagTool(server);
await registerGitWorktreeTool(server);
await registerGitWrapupInstructionsTool(server);
// Add calls to register other resources/tools here if needed in the future.
logger.info('Git tools registered successfully', context);
}
catch (err) {
// Registration is critical; log and re-throw errors.
logger.error('Failed to register resources/tools', {
...context,
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined, // Include stack for debugging
});
throw err; // Propagate error to prevent server starting with incomplete capabilities.
}
return server;
}
/**
* Selects, sets up, and starts the appropriate MCP transport layer based on configuration.
* This function acts as the bridge between the core server logic and the communication channel.
*
* MCP Spec Relevance:
* - Transport Selection: Reads `config.mcpTransportType` ('stdio' or 'http') to determine
* which transport mechanism defined in the MCP specification to use.
* - Transport Connection: Calls dedicated functions (`connectStdioTransport` or `startHttpTransport`)
* which handle the specifics of establishing communication according to the chosen
* transport's rules (e.g., stdin/stdout handling for 'stdio', HTTP server setup and
* endpoint handling for 'http').
* - Server Instance Lifecycle:
* - For 'stdio', creates a single `McpServer` instance for the lifetime of the process.
* - For 'http', passes the `createMcpServerInstance` factory function to `startHttpTransport`,
* allowing the HTTP transport to create a new, isolated server instance for each client session,
* aligning with the stateful session management described in the Streamable HTTP spec.
*
* @returns {Promise<McpServer | void>} Resolves with the McpServer instance for 'stdio', or void for 'http'.
* @throws {Error} If the configured transport type is unsupported or if transport setup fails.
*/
async function startTransport() {
// Determine the transport type from the validated configuration.
const transportType = config.mcpTransportType;
const context = { operation: 'startTransport', transport: transportType };
logger.info(`Starting transport: ${transportType}`, context);
// --- HTTP Transport Setup ---
if (transportType === 'http') {
logger.debug('Delegating to startHttpTransport...', context);
// For HTTP, the transport layer manages its own lifecycle and potentially multiple sessions.
// We pass the factory function to allow the HTTP transport to create server instances as needed (per session).
await startHttpTransport(createMcpServerInstance, context);
// The HTTP server runs indefinitely, listening for connections, so this function returns void.
return;
}
// --- Stdio Transport Setup ---
if (transportType === 'stdio') {
logger.debug('Creating single McpServer instance for stdio transport...', context);
// For stdio, there's typically one persistent connection managed by a parent process.
// Create a single McpServer instance for the entire process lifetime.
const server = await createMcpServerInstance();
logger.debug('Delegating to connectStdioTransport...', context);
// Connect the server instance to the stdio transport handler.
await connectStdioTransport(server, context);
// Return the server instance; the caller (main entry point) might hold onto it.
return server;
}
// --- Unsupported Transport ---
// This case should theoretically not be reached due to config validation, but acts as a safeguard.
logger.fatal(`Unsupported transport type configured: ${transportType}`, context);
throw new Error(`Unsupported transport type: ${transportType}. Must be 'stdio' or 'http'.`);
}
/**
* Main application entry point. Initializes and starts the MCP server.
*
* MCP Spec Relevance:
* - Orchestrates the server startup sequence, culminating in a server ready to accept
* connections and process MCP messages according to the chosen transport's rules.
* - Implements top-level error handling for critical startup failures, ensuring the
* process exits appropriately if it cannot initialize correctly.
*
* @returns {Promise<void | McpServer>} Resolves upon successful startup (void for http, McpServer for stdio). Rejects on critical failure.
*/
export async function initializeAndStartServer() {
const context = { operation: 'initializeAndStartServer' };
logger.info('MCP Server initialization sequence started.', context);
try {
// Initiate the transport setup based on configuration.
const result = await startTransport();
logger.info('MCP Server initialization sequence completed successfully.', context);
return result;
}
catch (err) {
// Catch any errors that occurred during server instance creation or transport setup.
logger.fatal('Critical error during MCP server initialization.', {
...context,
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
// Use the centralized error handler for consistent critical error reporting.
ErrorHandler.handleError(err, { ...context, critical: true });
// Exit the process with a non-zero code to indicate failure.
logger.info('Exiting process due to critical initialization error.', context);
process.exit(1);
}
}