Coverage for mcp/server.py: 92%

36 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-15 15:07 +0000

1""" 

2FastMCP server instance and instructions for the GCO MCP server. 

3 

4This module creates the shared ``mcp`` FastMCP instance that all tool and 

5resource modules register against. Import ``mcp`` from here — never create 

6a second instance. 

7""" 

8 

9import os 

10 

11from fastmcp import FastMCP 

12 

13# Code Mode lives under fastmcp.experimental — the import path itself signals 

14# the API can move between minor versions. The fastmcp pin in pyproject.toml is 

15# intentionally an `==` to keep that surface stable for a release. 

16from fastmcp.experimental.transforms.code_mode import ( 

17 CodeMode, 

18 GetSchemas, 

19 GetTags, 

20 MontySandboxProvider, 

21 Search, 

22) 

23from fastmcp.server.transforms import ResourcesAsTools 

24from fastmcp.server.transforms.search import BM25SearchTransform, RegexSearchTransform 

25 

26mcp = FastMCP( 

27 "GCO", 

28 instructions=( 

29 "Multi-region EKS Auto Mode platform for AI/ML workload orchestration. " 

30 "Submit jobs, manage inference endpoints, check capacity, track costs, " 

31 "and manage infrastructure across AWS regions.\n\n" 

32 "Resources available:\n" 

33 "- docs:// — Documentation, architecture guides, and example job/inference manifests\n" 

34 "- k8s:// — Kubernetes manifests deployed to the cluster (RBAC, deployments, NodePools, etc.)\n" 

35 "- iam:// — IAM policy templates for access control\n" 

36 "- infra:// — Dockerfiles, Helm charts, CI/CD config\n" 

37 "- ci:// — GitHub Actions workflows, composite actions, scripts, issue/PR templates\n" 

38 "- source:// — Full source code of the platform\n" 

39 "- demos:// — Demo walkthroughs, live demo scripts, and presentation materials\n" 

40 "- clients:// — API client examples (Python, curl, AWS CLI)\n" 

41 "- scripts:// — Utility scripts for cluster access, versioning, testing\n" 

42 "- tests:// — Test suite documentation, patterns, and configuration\n" 

43 "- config:// — CDK configuration schema, feature toggles, and environment variables\n\n" 

44 "Start with docs://gco/index or k8s://gco/manifests/index to explore." 

45 ), 

46 # NOTE on background-task support: ``tasks=True`` is intentionally NOT set 

47 # here. FastMCP's ``tasks=True`` at the server level applies a default 

48 # ``TaskConfig(mode="optional")`` to every tool, which requires every tool 

49 # function to be async (FastMCP raises ValueError at registration time 

50 # otherwise). The async migration of existing sync tools lands in a 

51 # later phase; until then, the long-running tools that genuinely need 

52 # background-task support set ``task=TaskConfig(mode=...)`` on their 

53 # individual ``@mcp.tool(...)`` decorators rather than relying on the 

54 # server-wide default. The ``fastmcp[tasks]`` extra is still pulled in 

55 # via ``pyproject.toml`` so pydocket is available when those per-tool 

56 # decorators run. 

57) 

58 

59# Always-on: tool-only clients (Cursor) get list_resources/read_resource synthetic tools. 

60# Registered AFTER the catalog-replacement transform below so the synthetic 

61# resource tools survive even when BM25/Regex/Code Mode replace the catalog. 

62 

63 

64def _int_env(name: str, default: int) -> int: 

65 """Parse an integer env var; fall back to default on missing/empty/non-numeric.""" 

66 raw = os.environ.get(name, "").strip() 

67 if not raw: 

68 return default 

69 try: 

70 return int(raw) 

71 except ValueError: 

72 return default 

73 

74 

75def _float_env(name: str, default: float) -> float: 

76 """Parse a float env var; fall back to default on missing/empty/non-numeric.""" 

77 raw = os.environ.get(name, "").strip() 

78 if not raw: 

79 return default 

80 try: 

81 return float(raw) 

82 except ValueError: 

83 return default 

84 

85 

86# Catalog-replacement transform. Mutually exclusive between the four values. 

87# Default is "bm25" so a brand-new install gets relevance-ranked tool search 

88# without any extra configuration. An unknown value (typo, etc.) also falls 

89# back to "bm25" so a misconfigured client doesn't accidentally drop into the 

90# full-catalog listing. 

91_TOOL_SEARCH = os.environ.get("GCO_MCP_TOOL_SEARCH", "bm25").strip().lower() 

92_ALWAYS_VISIBLE = [ 

93 "find_examples", 

94 "find_docs", 

95 "list_jobs", 

96 "submit_job_sqs", 

97 "list_inference_endpoints", 

98 "task_status", 

99] 

100if _TOOL_SEARCH == "bm25": 

101 mcp.add_transform(BM25SearchTransform(always_visible=_ALWAYS_VISIBLE)) 

102elif _TOOL_SEARCH == "regex": 102 ↛ 103line 102 didn't jump to line 103 because the condition on line 102 was never true

103 mcp.add_transform(RegexSearchTransform(always_visible=_ALWAYS_VISIBLE)) 

104elif _TOOL_SEARCH == "code_mode": 

105 # Four-stage discovery: GetTags → Search → GetSchemas → execute. Tags are 

106 # mandatory on every tool, so GetTags as the first stage gives the LLM 

107 # cheap browse-by-category before searching. 

108 mcp.add_transform( 

109 CodeMode( 

110 discovery_tools=[GetTags(), Search(), GetSchemas()], 

111 sandbox_provider=MontySandboxProvider( 

112 limits={ 

113 "max_duration_secs": _float_env("GCO_MCP_CODE_MODE_MAX_DURATION_SECS", 30.0), 

114 "max_memory": _int_env("GCO_MCP_CODE_MODE_MAX_MEMORY", 200_000_000), 

115 }, 

116 ), 

117 ) 

118 ) 

119elif _TOOL_SEARCH == "off": 

120 pass # legacy: list_tools returns the full catalog 

121else: 

122 # Unknown value → behave as the default (bm25). 

123 mcp.add_transform(BM25SearchTransform(always_visible=_ALWAYS_VISIBLE)) 

124 

125 

126# Resources As Tools is added AFTER the catalog-replacement transform so its 

127# synthetic ``list_resources`` / ``read_resource`` tools are appended to the 

128# catalog the search transform produced. Tool-only clients (Cursor, etc.) 

129# always see the resource surface even under search-mode. 

130mcp.add_transform(ResourcesAsTools(mcp)) 

131 

132 

133# Audit-capture middleware. Installs once after the transforms so every 

134# tool invocation gets fresh per-call buffers for ctx.warning/info/error 

135# and ctx.elicit. The patched Context methods are a no-op outside an 

136# active middleware scope, so this has no effect on non-MCP callers. 

137from audit_middleware import AuditCaptureMiddleware # noqa: E402 

138 

139mcp.add_middleware(AuditCaptureMiddleware())