Coverage for gco_mcp / server.py: 100.00%

43 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-09-14 22: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 

10import sys 

11 

12from fastmcp import FastMCP 

13 

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

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

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

17from fastmcp.experimental.transforms.code_mode import ( 

18 CodeMode, 

19 GetSchemas, 

20 GetTags, 

21 MontySandboxProvider, 

22 Search, 

23) 

24from fastmcp.server.transforms import ResourcesAsTools 

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

26 

27# Background tasks are the io.modelcontextprotocol/tasks protocol extension 

28# (SEP-2663) in FastMCP 4, shipped as the separate ``fastmcp-tasks`` package 

29# pulled in by the ``fastmcp[tasks]`` extra. A server with 

30# ``task=TaskConfig(...)`` tools (tools/stacks.py, tools/images.py) refuses 

31# to start unless the extension is registered below. 

32from fastmcp_tasks import TasksExtension 

33from version import get_project_version 

34 

35# ``server`` and ``gco_mcp.server`` are the only supported import names. 

36# Bind the counterpart before constructing FastMCP so both routes share one 

37# registry without a redundant third-name fallthrough. 

38_THIS_MODULE = sys.modules[__name__] 

39_OTHER_MODULE_NAME = { 

40 "server": "gco_mcp.server", 

41 "gco_mcp.server": "server", 

42}[__name__] 

43sys.modules.setdefault(_OTHER_MODULE_NAME, _THIS_MODULE) 

44 

45mcp = FastMCP( 

46 "GCO", 

47 version=get_project_version(), 

48 instructions=( 

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

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

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

52 "Resources available:\n" 

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

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

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

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

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

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

59 "- demos:// — Demo walkthroughs and live demo scripts\n" 

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

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

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

63 "- config:// — CDK configuration and environment variables\n" 

64 "- images:// — ECR repositories, tags, image details, and replication state\n" 

65 "- gco:// — Live regional jobs, Kubernetes objects, cluster topology, and inference state\n" 

66 "- costs:// and tasks:// — Windowed cost and background-task status views\n" 

67 "- mission:// — Mission sessions, reports, and audit replay (feature-gated)\n" 

68 "- mcp:// — Live tool/resource indexes and feature-flag mappings\n\n" 

69 "Start with docs://gco/index or mcp://gco/resources/index to explore." 

70 ), 

71 # NOTE on background-task support: the server-wide ``tasks=True`` kwarg is 

72 # intentionally NOT set here. It applies a default 

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

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

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

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

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

78 # individual ``@mcp.tool(...)`` decorators. Task execution itself is 

79 # provided by the SEP-2663 tasks extension registered right below. 

80) 

81 

82# FastMCP 4 moved background tasks out of the core protocol and into the 

83# io.modelcontextprotocol/tasks extension (SEP-2663). Registering the 

84# extension is mandatory: a server with ``task=TaskConfig(...)`` tools 

85# refuses to start without it. The default backend is the in-memory, 

86# single-process docket, which matches the stdio deployment model of this 

87# server; ``FASTMCP_DOCKET_URL`` (e.g. ``redis://...``) selects a durable 

88# backend that survives restarts and spans workers. 

89mcp.add_extension(TasksExtension()) 

90 

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

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

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

94 

95 

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

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

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

99 if not raw: 

100 return default 

101 try: 

102 return int(raw) 

103 except ValueError: 

104 return default 

105 

106 

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

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

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

110 if not raw: 

111 return default 

112 try: 

113 return float(raw) 

114 except ValueError: 

115 return default 

116 

117 

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

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

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

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

122# full-catalog listing. 

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

124_ALWAYS_VISIBLE = [ 

125 "find_examples", 

126 "find_docs", 

127 "list_jobs", 

128 "submit_job_sqs", 

129 "list_inference_endpoints", 

130 "check_capacity", 

131 "task_status", 

132 "fleet_status", 

133] 

134if _TOOL_SEARCH == "bm25": 

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

136elif _TOOL_SEARCH == "regex": 

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

138elif _TOOL_SEARCH == "code_mode": 

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

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

141 # cheap browse-by-category before searching. 

142 mcp.add_transform( 

143 CodeMode( 

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

145 sandbox_provider=MontySandboxProvider( 

146 limits={ 

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

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

149 }, 

150 ), 

151 ) 

152 ) 

153elif _TOOL_SEARCH == "off": 

154 pass # legacy: list_tools returns the full catalog 

155else: 

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

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

158 

159 

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

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

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

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

164mcp.add_transform(ResourcesAsTools(mcp)) 

165 

166 

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

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

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

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

171from audit_middleware import AuditCaptureMiddleware # noqa: E402 

172 

173mcp.add_middleware(AuditCaptureMiddleware())