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
« 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.
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# 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
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)
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)
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())
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.
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
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
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))
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))
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
173mcp.add_middleware(AuditCaptureMiddleware())