Coverage for gco_mcp/server.py: 84.48%
42 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 21:22 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 21:22 +0000
1"""
2FastMCP server instance and instructions for the GCO MCP server.
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"""
9import os
10import sys
12from fastmcp import FastMCP
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
27# ``run_mcp`` supports both legacy top-level imports (``import server`` after
28# adding gco_mcp/ to sys.path) and package imports. Bind both names to the first
29# loaded module before constructing the FastMCP object so the two routes can
30# never create independent registries.
31_THIS_MODULE = sys.modules[__name__]
32if __name__ == "server": 32 ↛ 34line 32 didn't jump to line 34 because the condition on line 32 was always true
33 sys.modules.setdefault("gco_mcp.server", _THIS_MODULE)
34elif __name__ == "gco_mcp.server":
35 sys.modules.setdefault("server", _THIS_MODULE)
37mcp = FastMCP(
38 "GCO",
39 instructions=(
40 "Multi-region EKS Auto Mode platform for AI/ML workload orchestration. "
41 "Submit jobs, manage inference endpoints, check capacity, track costs, "
42 "and manage infrastructure across AWS regions.\n\n"
43 "Resources available:\n"
44 "- docs:// — Documentation, architecture guides, and example job/inference manifests\n"
45 "- k8s:// — Kubernetes manifests deployed to the cluster (RBAC, deployments, NodePools, etc.)\n"
46 "- iam:// — IAM policy templates for access control\n"
47 "- infra:// — Dockerfiles, Helm charts, CI/CD config\n"
48 "- ci:// — GitHub Actions workflows, composite actions, scripts, issue/PR templates\n"
49 "- source:// — Full source code of the platform\n"
50 "- demos:// — Demo walkthroughs, live demo scripts, and presentation materials\n"
51 "- clients:// — API client examples (Python, curl, AWS CLI)\n"
52 "- scripts:// — Utility scripts for cluster access, versioning, testing\n"
53 "- tests:// — Test suite documentation, patterns, and configuration\n"
54 "- config:// — CDK configuration and environment variables\n"
55 "- images:// — ECR repositories, tags, image details, and replication state\n"
56 "- gco:// — Live regional jobs, Kubernetes objects, cluster topology, and inference state\n"
57 "- costs:// and tasks:// — Windowed cost and background-task status views\n"
58 "- mission:// — Mission sessions, reports, and audit replay (feature-gated)\n"
59 "- mcp:// — Live tool/resource indexes and feature-flag mappings\n\n"
60 "Start with docs://gco/index or mcp://gco/resources/index to explore."
61 ),
62 # NOTE on background-task support: ``tasks=True`` is intentionally NOT set
63 # here. FastMCP's ``tasks=True`` at the server level applies a default
64 # ``TaskConfig(mode="optional")`` to every tool, which requires every tool
65 # function to be async (FastMCP raises ValueError at registration time
66 # otherwise). The async migration of existing sync tools lands in a
67 # later phase; until then, the long-running tools that genuinely need
68 # background-task support set ``task=TaskConfig(mode=...)`` on their
69 # individual ``@mcp.tool(...)`` decorators rather than relying on the
70 # server-wide default. The ``fastmcp[tasks]`` extra is still pulled in
71 # via ``pyproject.toml`` so pydocket is available when those per-tool
72 # decorators run.
73)
75# Always-on: tool-only clients (Cursor) get list_resources/read_resource synthetic tools.
76# Registered AFTER the catalog-replacement transform below so the synthetic
77# resource tools survive even when BM25/Regex/Code Mode replace the catalog.
80def _int_env(name: str, default: int) -> int:
81 """Parse an integer env var; fall back to default on missing/empty/non-numeric."""
82 raw = os.environ.get(name, "").strip()
83 if not raw:
84 return default
85 try:
86 return int(raw)
87 except ValueError:
88 return default
91def _float_env(name: str, default: float) -> float:
92 """Parse a float env var; fall back to default on missing/empty/non-numeric."""
93 raw = os.environ.get(name, "").strip()
94 if not raw:
95 return default
96 try:
97 return float(raw)
98 except ValueError:
99 return default
102# Catalog-replacement transform. Mutually exclusive between the four values.
103# Default is "bm25" so a brand-new install gets relevance-ranked tool search
104# without any extra configuration. An unknown value (typo, etc.) also falls
105# back to "bm25" so a misconfigured client doesn't accidentally drop into the
106# full-catalog listing.
107_TOOL_SEARCH = os.environ.get("GCO_MCP_TOOL_SEARCH", "bm25").strip().lower()
108_ALWAYS_VISIBLE = [
109 "find_examples",
110 "find_docs",
111 "list_jobs",
112 "submit_job_sqs",
113 "list_inference_endpoints",
114 "check_capacity",
115 "task_status",
116]
117if _TOOL_SEARCH == "bm25":
118 mcp.add_transform(BM25SearchTransform(always_visible=_ALWAYS_VISIBLE))
119elif _TOOL_SEARCH == "regex": 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true
120 mcp.add_transform(RegexSearchTransform(always_visible=_ALWAYS_VISIBLE))
121elif _TOOL_SEARCH == "code_mode":
122 # Four-stage discovery: GetTags → Search → GetSchemas → execute. Tags are
123 # mandatory on every tool, so GetTags as the first stage gives the LLM
124 # cheap browse-by-category before searching.
125 mcp.add_transform(
126 CodeMode(
127 discovery_tools=[GetTags(), Search(), GetSchemas()],
128 sandbox_provider=MontySandboxProvider(
129 limits={
130 "max_duration_secs": _float_env("GCO_MCP_CODE_MODE_MAX_DURATION_SECS", 30.0),
131 "max_memory": _int_env("GCO_MCP_CODE_MODE_MAX_MEMORY", 200_000_000),
132 },
133 ),
134 )
135 )
136elif _TOOL_SEARCH == "off":
137 pass # legacy: list_tools returns the full catalog
138else:
139 # Unknown value → behave as the default (bm25).
140 mcp.add_transform(BM25SearchTransform(always_visible=_ALWAYS_VISIBLE))
143# Resources As Tools is added AFTER the catalog-replacement transform so its
144# synthetic ``list_resources`` / ``read_resource`` tools are appended to the
145# catalog the search transform produced. Tool-only clients (Cursor, etc.)
146# always see the resource surface even under search-mode.
147mcp.add_transform(ResourcesAsTools(mcp))
150# Audit-capture middleware. Installs once after the transforms so every
151# tool invocation gets fresh per-call buffers for ctx.warning/info/error
152# and ctx.elicit. The patched Context methods are a no-op outside an
153# active middleware scope, so this has no effect on non-MCP callers.
154from audit_middleware import AuditCaptureMiddleware # noqa: E402
156mcp.add_middleware(AuditCaptureMiddleware())