Coverage for mcp/server.py: 92%
36 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:07 +0000
« 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.
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
11from fastmcp import FastMCP
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
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)
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.
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
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
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))
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))
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
139mcp.add_middleware(AuditCaptureMiddleware())