-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver_application.py
More file actions
2158 lines (1828 loc) · 76.5 KB
/
Copy pathserver_application.py
File metadata and controls
2158 lines (1828 loc) · 76.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Main MCP Git Server Application.
This module provides the ServerApplication class that serves as the primary entry point
and orchestrator for the entire MCP Git server system. It integrates all decomposed
components into a cohesive, production-ready application.
The ServerApplication replaces the monolithic server.py approach with a clean,
component-based architecture that provides the same functionality with improved
structure, maintainability, and LLM compatibility.
"""
import asyncio
import logging
import os
import signal
import sys
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from datetime import datetime
from pathlib import Path
from typing import Any, Literal
from mcp.types import Tool
from pydantic import BaseModel, Field, field_validator
from ..frameworks.mcp_server_framework import MCPServerFramework
from ..frameworks.server_configuration import ServerConfigurationManager
from ..frameworks.server_core import MCPGitServerCore
from ..frameworks.server_github import GitHubService
from ..frameworks.server_middleware import MiddlewareChainManager
from ..frameworks.server_security import SecurityFramework
from ..operations.server_notifications import NotificationOperations
from ..protocols.debugging_protocol import DebuggableComponent
from ..services.git_service import GitService
from ..services.github_service import GitHubServiceConfig
from ..services.server_metrics import MetricsService
from ..services.server_session import SessionManager
# Azure DevOps models for tool registration
from ..azure.models import (
AzureGetBuildLogs,
AzureGetBuildStatus,
AzureGetFailingJobs,
AzureListBuilds,
)
logger = logging.getLogger(__name__)
# ===== MCP TOOL MODELS =====
# Import the tool models from the original server to maintain compatibility
from enum import Enum
class GitTools(str, Enum):
"""Git tool names."""
STATUS = "git_status"
DIFF_UNSTAGED = "git_diff_unstaged"
DIFF_STAGED = "git_diff_staged"
DIFF = "git_diff"
COMMIT = "git_commit"
ADD = "git_add"
RESET = "git_reset"
LOG = "git_log"
CREATE_BRANCH = "git_create_branch"
CHECKOUT = "git_checkout"
SHOW = "git_show"
INIT = "git_init"
PUSH = "git_push"
PULL = "git_pull"
DIFF_BRANCHES = "git_diff_branches"
REBASE = "git_rebase"
MERGE = "git_merge"
CHERRY_PICK = "git_cherry_pick"
ABORT = "git_abort"
CONTINUE = "git_continue"
BRANCH_LIST = "git_branch_list"
FETCH = "git_fetch"
REMOTE_ADD = "git_remote_add"
REMOTE_REMOVE = "git_remote_remove"
REMOTE_LIST = "git_remote_list"
REMOTE_GET_URL = "git_remote_get_url"
class GitStatus(BaseModel):
repo_path: str
class GitDiffUnstaged(BaseModel):
repo_path: str
class GitDiffStaged(BaseModel):
repo_path: str
class GitDiff(BaseModel):
repo_path: str
target: str
class GitCommit(BaseModel):
repo_path: str
message: str
gpg_sign: bool = False
gpg_key_id: str | None = None
class GitAdd(BaseModel):
repo_path: str
files: list[str]
class GitReset(BaseModel):
repo_path: str
mode: str = "mixed" # soft, mixed, hard
target: str | None = None
class GitLog(BaseModel):
repo_path: str
max_count: int = 10
class GitCreateBranch(BaseModel):
repo_path: str
branch_name: str
base_branch: str | None = None
class GitCheckout(BaseModel):
repo_path: str
branch_name: str
class GitShow(BaseModel):
repo_path: str
revision: str
class GitInit(BaseModel):
repo_path: str
class GitPush(BaseModel):
repo_path: str
remote: str = "origin"
branch: str | None = None
force: bool = False
class GitPull(BaseModel):
repo_path: str
remote: str = "origin"
branch: str | None = None
class GitDiffBranches(BaseModel):
repo_path: str
base_branch: str
target_branch: str
class GitRebase(BaseModel):
repo_path: str
target_branch: str
class GitMerge(BaseModel):
repo_path: str
source_branch: str
strategy: str = "merge"
message: str | None = None
class GitCherryPick(BaseModel):
repo_path: str
commit_hash: str
no_commit: bool = False
class GitAbort(BaseModel):
repo_path: str
operation: Literal["rebase", "merge", "cherry-pick"] = Field(
..., description="The git operation to abort"
)
class GitContinue(BaseModel):
repo_path: str
operation: Literal["rebase", "merge", "cherry-pick"] = Field(
..., description="The git operation to continue"
)
class GitFetch(BaseModel):
repo_path: str
remote: str = "origin"
class GitRemoteAdd(BaseModel):
repo_path: str
name: str
url: str
class GitRemoteRemove(BaseModel):
repo_path: str
name: str
class GitRemoteList(BaseModel):
repo_path: str
class GitRemoteGetUrl(BaseModel):
repo_path: str
name: str
class GitHubTools(str, Enum):
"""GitHub tool names."""
CREATE_ISSUE = "github_create_issue"
LIST_ISSUES = "github_list_issues"
UPDATE_ISSUE = "github_update_issue"
GET_PR_CHECKS = "github_get_pr_checks"
GET_PR_DETAILS = "github_get_pr_details"
LIST_PULL_REQUESTS = "github_list_pull_requests"
GET_PR_STATUS = "github_get_pr_status"
GET_PR_FILES = "github_get_pr_files"
EDIT_PR_DESCRIPTION = "github_edit_pr_description"
GET_WORKFLOW_RUN = "github_get_workflow_run"
LIST_WORKFLOW_RUNS = "github_list_workflow_runs"
AWAIT_WORKFLOW_COMPLETION = "github_await_workflow_completion"
CREATE_PR = "github_create_pr"
MERGE_PR = "github_merge_pr"
ADD_PR_COMMENT = "github_add_pr_comment"
CLOSE_PR = "github_close_pr"
REOPEN_PR = "github_reopen_pr"
UPDATE_PR = "github_update_pr"
GET_FAILING_JOBS = "github_get_failing_jobs"
class AzureTools(str, Enum):
"""Azure DevOps tool names."""
GET_BUILD_STATUS = "azure_get_build_status"
GET_BUILD_LOGS = "azure_get_build_logs"
GET_FAILING_JOBS = "azure_get_failing_jobs"
LIST_BUILDS = "azure_list_builds"
class GitHubCreateIssue(BaseModel):
repo_owner: str
repo_name: str
title: str
body: str | None = None
labels: list[str] | None = None
assignees: list[str] | None = None
milestone: int | None = None
class GitHubListIssues(BaseModel):
repo_owner: str
repo_name: str
state: str = "open" # open, closed, all
labels: str | None = None
assignee: str | None = None
creator: str | None = None
mentioned: str | None = None
milestone: str | None = None
sort: str = "created" # created, updated, comments
direction: str = "desc" # asc, desc
since: str | None = None
per_page: int = 30
page: int = 1
class GitHubUpdateIssue(BaseModel):
repo_owner: str
repo_name: str
issue_number: int
title: str | None = None
body: str | None = None
state: str | None = None # open, closed
labels: list[str] | None = None
assignees: list[str] | None = None
milestone: int | None = None
class GitHubGetPrChecks(BaseModel):
repo_owner: str
repo_name: str
pr_number: int
status: str | None = None
conclusion: str | None = None
class GitHubGetPrDetails(BaseModel):
repo_owner: str
repo_name: str
pr_number: int
include_files: bool = False
include_reviews: bool = False
class GitHubListPullRequests(BaseModel):
repo_owner: str
repo_name: str
state: str = "open" # open, closed, all
head: str | None = None
base: str | None = None
sort: str = "created" # created, updated, popularity
direction: str = "desc" # asc, desc
per_page: int = 30
page: int = 1
class GitHubGetPrStatus(BaseModel):
repo_owner: str
repo_name: str
pr_number: int
class GitHubGetPrFiles(BaseModel):
repo_owner: str
repo_name: str
pr_number: int
per_page: int = 30
page: int = 1
include_patch: bool = False
class GitHubEditPrDescription(BaseModel):
repo_owner: str
repo_name: str
pr_number: int
description: str
class GitHubGetWorkflowRun(BaseModel):
repo_owner: str
repo_name: str
run_id: int
include_logs: bool = False
class GitHubListWorkflowRuns(BaseModel):
repo_owner: str
repo_name: str
workflow_id: str | None = None
actor: str | None = None
branch: str | None = None
event: str | None = None
status: str | None = None
conclusion: str | None = None
per_page: int = 30
page: int = 1
created: str | None = None
exclude_pull_requests: bool = False
check_suite_id: int | None = None
head_sha: str | None = None
@field_validator("per_page")
@classmethod
def validate_per_page(cls, v: int) -> int:
"""Enforce GitHub API limits for per_page parameter."""
return max(1, min(v, 100))
@field_validator("page")
@classmethod
def validate_page(cls, v: int) -> int:
"""Ensure page number is positive."""
return max(1, v)
@field_validator("status")
@classmethod
def validate_status(cls, v: str | None) -> str | None:
"""Validate workflow run status values."""
if v is None:
return v
valid_statuses = {
"queued",
"in_progress",
"completed",
"waiting",
"requested",
"pending",
}
if v not in valid_statuses:
raise ValueError(
f"status must be one of: {', '.join(sorted(valid_statuses))}"
)
return v
@field_validator("conclusion")
@classmethod
def validate_conclusion(cls, v: str | None) -> str | None:
"""Validate workflow run conclusion values."""
if v is None:
return v
valid_conclusions = {
"success",
"failure",
"neutral",
"cancelled",
"timed_out",
"action_required",
"stale",
"startup_failure",
}
if v not in valid_conclusions:
raise ValueError(
f"conclusion must be one of: {', '.join(sorted(valid_conclusions))}"
)
return v
class GitHubAwaitWorkflowCompletion(BaseModel):
repo_owner: str
repo_name: str
run_id: int | None = None # None = latest run
timeout_minutes: int = 15
poll_interval_seconds: int = 20
@field_validator("timeout_minutes")
@classmethod
def validate_timeout(cls, v: int) -> int:
"""Ensure timeout is reasonable (1-350 minutes / 5h 50m)."""
if v < 1 or v > 350:
raise ValueError("timeout_minutes must be between 1 and 350")
return v
@field_validator("poll_interval_seconds")
@classmethod
def validate_poll_interval(cls, v: int) -> int:
"""Ensure poll interval is reasonable (5-120 seconds)."""
if v < 5 or v > 120:
raise ValueError("poll_interval_seconds must be between 5 and 120")
return v
class GitHubCreatePr(BaseModel):
repo_owner: str
repo_name: str
title: str
head: str
base: str
body: str | None = None
draft: bool = False
class GitHubMergePr(BaseModel):
repo_owner: str
repo_name: str
pr_number: int
commit_title: str | None = None
commit_message: str | None = None
merge_method: str = "merge"
class GitHubAddPrComment(BaseModel):
repo_owner: str
repo_name: str
pr_number: int
body: str
class GitHubClosePr(BaseModel):
repo_owner: str
repo_name: str
pr_number: int
class GitHubReopenPr(BaseModel):
repo_owner: str
repo_name: str
pr_number: int
class GitHubUpdatePr(BaseModel):
repo_owner: str
repo_name: str
pr_number: int
title: str | None = None
body: str | None = None
state: str | None = None
base: str | None = None
class GitHubGetFailingJobs(BaseModel):
repo_owner: str
repo_name: str
pr_number: int
include_logs: bool = True
include_annotations: bool = True
class ServerApplicationConfig:
"""Configuration for the server application."""
def __init__(
self,
repository_path: Path | None = None,
enable_metrics: bool = True,
enable_security: bool = True,
enable_notifications: bool = True,
test_mode: bool = False,
debug_mode: bool = False,
):
"""
Initialize server application configuration.
Args:
repository_path: Optional path to the git repository to serve
enable_metrics: Whether to enable metrics collection
enable_security: Whether to enable security framework
enable_notifications: Whether to enable notification operations
test_mode: Whether to run in test mode
debug_mode: Whether to enable debug mode
"""
self.repository_path = repository_path
self.enable_metrics = enable_metrics
self.enable_security = enable_security
self.enable_notifications = enable_notifications
self.test_mode = test_mode
self.debug_mode = debug_mode
class ServerApplication(DebuggableComponent):
"""
Main MCP Git Server Application.
This class orchestrates all components of the MCP Git server into a cohesive
application. It handles initialization, startup, runtime management, and
graceful shutdown of all subsystems.
The application follows a component-based architecture where each major
subsystem is represented by a dedicated component that can be independently
configured, started, stopped, and debugged.
Example usage:
>>> config = ServerApplicationConfig(
... repository_path=Path("/path/to/repo"),
... enable_metrics=True
... )
>>> app = ServerApplication(config)
>>> await app.start()
>>> # Server is now running
>>> await app.stop()
"""
def __init__(self, config: ServerApplicationConfig | None = None):
"""
Initialize the server application.
Args:
config: Application configuration
"""
self.config = config or ServerApplicationConfig()
self._initialized = False
self._running = False
self._shutdown_event = asyncio.Event()
self._components: dict[str, Any] = {}
# Core framework components
self._framework: MCPServerFramework | None = None
self._server_core: MCPGitServerCore | None = None
self._configuration_manager: ServerConfigurationManager | None = None
# Service components
self._git_service: GitService | None = None
self._github_service: GitHubService | None = None
self._metrics_service: MetricsService | None = None
self._session_manager: SessionManager | None = None
# Operations components
self._notification_operations: NotificationOperations | None = None
# Infrastructure components
self._middleware_manager: MiddlewareChainManager | None = None
self._security_framework: SecurityFramework | None = None
logger.info("ServerApplication initialized")
async def initialize(self) -> None:
"""
Initialize all application components.
This method sets up all components in the correct order, ensuring
that dependencies are satisfied and all systems are ready to start.
"""
if self._initialized:
logger.warning("ServerApplication already initialized")
return
logger.info("Initializing ServerApplication components...")
try:
# Phase 1: Initialize core framework
await self._initialize_core_framework()
# Phase 2: Initialize configuration management
await self._initialize_configuration()
# Phase 3: Initialize infrastructure components
await self._initialize_infrastructure()
# Phase 4: Initialize services
await self._initialize_services()
# Phase 5: Initialize operations
await self._initialize_operations()
# Phase 6: Register all components with framework
await self._register_components()
# Phase 7: Initialize the framework
await self._framework.initialize()
self._initialized = True
logger.info("ServerApplication initialization complete")
except Exception as e:
logger.error(f"Failed to initialize ServerApplication: {e}")
await self._cleanup_partial_initialization()
raise
async def _initialize_core_framework(self) -> None:
"""Initialize the core MCP framework."""
logger.debug("Initializing core framework...")
# Create the main framework
self._framework = MCPServerFramework()
# Create the server core
self._server_core = MCPGitServerCore("mcp-git-server")
# Initialize the MCP server within the core
self._server_core.initialize_server(self.config.repository_path)
# Register MCP tools with the server
await self._register_mcp_tools()
logger.debug("Core framework initialized")
async def _initialize_configuration(self) -> None:
"""Initialize configuration management."""
logger.debug("Initializing configuration management...")
self._configuration_manager = ServerConfigurationManager()
# Initialize with default configuration
await self._configuration_manager.initialize()
# Get the validated configuration
self._configuration_manager.get_current_config()
logger.debug("Configuration management initialized")
async def _initialize_infrastructure(self) -> None:
"""Initialize infrastructure components."""
logger.debug("Initializing infrastructure components...")
# Initialize security framework
if self.config.enable_security:
self._security_framework = SecurityFramework()
logger.debug("Security framework initialized")
# Initialize middleware management with enhanced token limit middleware
from ..frameworks.server_middleware import create_enhanced_middleware_chain
self._middleware_manager = create_enhanced_middleware_chain(
enable_token_limits=True
)
logger.debug("Enhanced middleware chain initialized with token limits")
logger.debug("Infrastructure components initialized")
async def _initialize_services(self) -> None:
"""Initialize service components."""
logger.debug("Initializing service components...")
# Get configuration for services
server_config = self._configuration_manager.get_current_config()
# Initialize Git service (convert server_config to GitServiceConfig)
from ..services.git_service import GitServiceConfig
git_config = GitServiceConfig(
max_concurrent_operations=server_config.max_concurrent_operations,
operation_timeout_seconds=server_config.operation_timeout_seconds,
enable_security_validation=server_config.enable_security_validation,
)
self._git_service = GitService(config=git_config)
# Initialize GitHub service
github_config = GitHubServiceConfig(
github_token=server_config.github_token,
)
self._github_service = GitHubService(github_config)
# Initialize metrics service
if self.config.enable_metrics:
self._metrics_service = MetricsService(
enable_system_metrics=True, max_metric_history=1000
)
logger.debug("Metrics service initialized")
# Initialize session management
self._session_manager = SessionManager()
logger.debug("Session manager initialized")
logger.debug("Service components initialized")
async def _initialize_operations(self) -> None:
"""Initialize operations components."""
logger.debug("Initializing operations components...")
# Initialize notification operations - TEMPORARILY DISABLED due to abstract methods
# TODO: Fix NotificationOperations abstract method implementations
# if self.config.enable_notifications:
# server_config = self._configuration_manager.get_current_config()
# self._notification_operations = NotificationOperations(server_config)
# logger.debug("Notification operations initialized")
logger.debug("Operations components initialized")
async def _register_components(self) -> None:
"""Register all components with the framework."""
logger.debug("Registering components with framework...")
# Register core components
if self._server_core:
self._framework.register_component(
name="server_core",
component=self._server_core,
dependencies=[],
priority=1,
)
if self._configuration_manager:
self._framework.register_component(
name="configuration",
component=self._configuration_manager,
dependencies=[],
priority=2,
)
# Register infrastructure components
if self._security_framework:
self._framework.register_component(
name="security",
component=self._security_framework,
dependencies=["configuration"],
priority=3,
)
if self._middleware_manager:
self._framework.register_component(
name="middleware",
component=self._middleware_manager,
dependencies=["configuration"],
priority=4,
)
# Register services
if self._git_service:
self._framework.register_component(
name="git_service",
component=self._git_service,
dependencies=["configuration", "security"],
priority=5,
)
if self._github_service:
self._framework.register_component(
name="github_service",
component=self._github_service,
dependencies=["configuration", "security"],
priority=6,
)
if self._metrics_service:
self._framework.register_component(
name="metrics",
component=self._metrics_service,
dependencies=["configuration"],
priority=7,
)
if self._session_manager:
self._framework.register_component(
name="sessions",
component=self._session_manager,
dependencies=["configuration"],
priority=8,
)
# Register operations
if self._notification_operations:
self._framework.register_component(
name="notifications",
component=self._notification_operations,
dependencies=["configuration"],
priority=9,
)
logger.debug("Component registration complete")
async def start(self) -> None:
"""
Start the server application.
This method starts all components in the correct order and begins
serving MCP requests. The method will block until the server is
shut down.
"""
if not self._initialized:
await self.initialize()
if self._running:
logger.warning("ServerApplication already running")
return
logger.info("Starting ServerApplication...")
try:
# Start the framework (which starts all components)
await self._framework.start()
# Start the MCP server core
if self._server_core:
self._running = True
logger.info("ServerApplication started successfully")
# Install signal handlers for graceful shutdown
self._install_signal_handlers()
# Start server core - this blocks until client disconnects or shutdown signal
await self._server_core.start_server(test_mode=self.config.test_mode)
# If we reach here, server core completed (client disconnected)
logger.info(
"🔁 Server core completed - client disconnected, shutting down"
)
# Set shutdown event to signal completion
self._shutdown_event.set()
except Exception as e:
logger.error(f"Failed to start ServerApplication: {e}")
await self.stop()
# In test mode, don't re-raise exceptions - exit gracefully for CI
if self.config.test_mode:
logger.warning(
f"🧪 Test mode: Application error handled gracefully: {e}"
)
return
else:
raise
async def stop(self) -> None:
"""
Stop the server application gracefully.
This method stops all components in reverse order and cleans up
all resources.
"""
if not self._running:
logger.warning("ServerApplication not running")
return
logger.info("Stopping ServerApplication...")
try:
# Signal shutdown
self._shutdown_event.set()
# Stop the framework (which stops all components)
if self._framework:
await self._framework.stop()
self._running = False
logger.info("ServerApplication stopped successfully")
except Exception as e:
logger.error(f"Error during ServerApplication shutdown: {e}")
raise
async def restart(self) -> None:
"""
Restart the server application.
This method performs a graceful stop followed by a start.
"""
logger.info("Restarting ServerApplication...")
await self.stop()
await self.start()
def _install_signal_handlers(self) -> None:
"""Install signal handlers for graceful shutdown."""
def signal_handler(signum: int, frame: Any) -> None:
logger.info(f"Received signal {signum}, initiating graceful shutdown...")
asyncio.create_task(self.stop())
if sys.platform != "win32":
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)
async def _cleanup_partial_initialization(self) -> None:
"""Clean up after partial initialization failure."""
logger.debug("Cleaning up partial initialization...")
# Stop any components that were started
if self._framework:
try:
await self._framework.stop()
except Exception as e:
logger.error(f"Error stopping framework during cleanup: {e}")
# Reset initialization state
self._initialized = False
@asynccontextmanager
async def _component_context(self, name: str, component: Any) -> AsyncIterator[Any]:
"""Context manager for component lifecycle."""
logger.debug(f"Starting component: {name}")
try:
if hasattr(component, "start"):
await component.start()
yield component
except Exception as e:
logger.error(f"Error in component {name}: {e}")
raise
finally:
logger.debug(f"Stopping component: {name}")
if hasattr(component, "stop"):
try:
await component.stop()
except Exception as e:
logger.error(f"Error stopping component {name}: {e}")
# DebuggableComponent Protocol Implementation
def get_component_state(self) -> dict[str, Any]:
"""Get current state of the server application."""
component_states = {}
# Collect state from all registered components
if self._framework:
for name, registration in self._framework._components.items():
component = registration.component
if hasattr(component, "get_component_state"):
try:
component_states[name] = component.get_component_state()
except Exception as e:
component_states[name] = {"error": str(e)}
else:
component_states[name] = {"status": "no_state_available"}
return {
"application_id": "mcp_git_server_application",
"status": "running" if self._running else "stopped",
"initialized": self._initialized,
"config": {
"repository_path": str(self.config.repository_path)
if self.config.repository_path
else None,
"enable_metrics": self.config.enable_metrics,
"enable_security": self.config.enable_security,
"enable_notifications": self.config.enable_notifications,
"test_mode": self.config.test_mode,
"debug_mode": self.config.debug_mode,
},
"components": component_states,
"component_count": len(component_states),
}
def validate_component(self) -> dict[str, Any]:
"""Validate server application configuration and state."""
issues = []
recommendations = []
# Check initialization state
if not self._initialized:
issues.append("Application not initialized")
# Check component availability
if not self._framework:
issues.append("Core framework not available")
if not self._server_core:
issues.append("Server core not available")