Closes #931.
The transport was a literal passed once at the bottom of the module,
bind_native_mcp_transport(transport="stdio"), and every guard that asks
"is this a trusted native session" resolves that question through the
value bound there. The string was a constant in the authorization chain
rather than configuration, so a remote transport had no way to be
expressed and no way to be told apart from an untrusted offline import.
mcp_transport_config becomes the single source of truth: it owns the
permitted set, the default, and the resolution of deployment
configuration (GITEA_MCP_TRANSPORT) into a validated identifier. Unset
configuration still yields stdio, so existing deployments are unchanged.
streamable-http is accepted so the bind is pluggable; standing up its
listener remains #938. sse is deliberately not registered.
bind_native_mcp_transport now resolves from configuration when no
transport argument is given, validates against that one permitted set
before writing the runtime record, and pins the result. bound_transport
is the shared accessor every transport-aware guard reads; it reports the
pinned value and never the environment, so a post-bind GITEA_MCP_TRANSPORT
change cannot move what a guard observes -- the rule already applied to
the session-state root under #695 AC2. Rebinding the same identifier is
idempotent; rebinding a different one is refused, so two guards can never
disagree within one process. assert_transport_bound fails closed before
mcp.run, so an absent or invalid bind stops the server instead of serving
tools over a transport no guard can name.
The bound identifier is now recorded in durable provenance:
mutation_provenance_fields gains bound_transport, which reaches the
decision lock through mcp_session_state.save_state and the audit records
that already spread those fields. The pre-existing transport field keeps
its trust-class values, so no durable record changes shape.
assess_transport_for_auth_mint reports the bound transport through the
same accessor; its verdict is unchanged.
#956's anchor fixture and threat model are re-anchored for the lines this
change shifts, and the two boundary claims that #931 makes false -- the
"literal stdio bind" in B1 and the stdio contract at mcp_server.py:4 --
are restated. Without this the #956 guard fails, which is what it is for.
Non-goals, untouched: the remote listener (#938), per-request principal
resolution (#932), client-managed provenance policy (#934), TLS and
remote client authentication, role capability sets, repository-binding
semantics.
Tests: tests/test_issue_931_transport_bind_seam.py, 42 tests, covering
default bind, explicit stdio, the accepted non-stdio identifier, an
unregistered identifier, no bind at all, one-value agreement across
guards, post-bind environment tampering, the durable decision-lock
record, the rebind contract, and the #695 protections that must not
regress.
Full suite from the branches/ worktree: 28 failed, 5843 passed, 6
skipped, 1047 subtests. The pinned base 9b80e75c reports 28 failed, 5801
passed, 6 skipped, 1047 subtests. The failing test IDs are identical sets
-- no failure added, none fixed; the +42 passed are this PR's new module.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
80 lines
3.2 KiB
Python
80 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Gitea MCP Server — exposes Gitea operations as MCP tools.
|
|
|
|
The transport is selected by deployment configuration (GITEA_MCP_TRANSPORT) and
|
|
defaults to the local client-spawned transport when unset (#931); the permitted
|
|
set lives in mcp_transport_config. All tools authenticate via macOS keychain
|
|
(git credential fill).
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
if "PYTEST_CURRENT_TEST" not in os.environ:
|
|
sys.stderr = open("/tmp/mcp_server_stderr.log", "a", buffering=1)
|
|
sys.stderr.write(f"\n--- MCP SERVER STARTUP (PID {os.getpid()}) ---\n")
|
|
|
|
from role_session_router import (
|
|
python_bytes_have_conflict_markers,
|
|
skip_python_scan_walk_root,
|
|
)
|
|
|
|
|
|
# Startup health check: scan all python files in the Gitea-Tools directory for unresolved conflict markers.
|
|
def check_conflict_markers():
|
|
dir_path = os.path.dirname(os.path.abspath(__file__))
|
|
for root, dirs, files in os.walk(dir_path):
|
|
if skip_python_scan_walk_root(dir_path, root):
|
|
continue
|
|
for file in files:
|
|
if file.endswith(".py"):
|
|
file_path = os.path.join(root, file)
|
|
try:
|
|
with open(file_path, "rb") as f:
|
|
if python_bytes_have_conflict_markers(f.read()):
|
|
rel_path = os.path.relpath(file_path, dir_path)
|
|
print(
|
|
f"infra_stop: Unresolved merge conflict detected in {rel_path}. "
|
|
"Please resolve all conflicts manually, finish/abort the merge, "
|
|
"restart the MCP server, and retry.",
|
|
file=sys.stderr
|
|
)
|
|
sys.exit(1)
|
|
except Exception:
|
|
pass
|
|
|
|
check_conflict_markers()
|
|
|
|
# #558 / #695: claim the official entrypoint before loading mutation modules.
|
|
# This alone does NOT authorize mutations — gitea_mcp_server binds the live
|
|
# native MCP transport immediately before mcp.run, over the configured
|
|
# transport (#931). Import-only or offline launch without that bind fails
|
|
# closed on mutations.
|
|
try:
|
|
import mcp_daemon_guard
|
|
|
|
mcp_daemon_guard.mark_sanctioned_daemon()
|
|
except Exception:
|
|
# Guard import failures must not hide conflict-marker infra_stop above;
|
|
# gitea_mcp_server main also marks + binds when run as __main__.
|
|
pass
|
|
|
|
# Execute the actual server logic via exec in this namespace.
|
|
impl_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "gitea_mcp_server.py")
|
|
try:
|
|
with open(impl_path, "rb") as f:
|
|
# Override __file__ global so that inspect.getsource(mcp_server) returns the implementation source
|
|
globals()["__file__"] = impl_path
|
|
code = compile(f.read(), impl_path, "exec")
|
|
exec(code, globals())
|
|
except Exception as e:
|
|
if isinstance(e, SyntaxError):
|
|
# Fallback if a syntax error/conflict marker was introduced in gitea_mcp_server.py
|
|
print(
|
|
f"infra_stop: Unresolved merge conflict or syntax error in gitea_mcp_server.py. "
|
|
"Please resolve all conflicts manually, finish/abort the merge, "
|
|
"restart the MCP server, and retry.",
|
|
file=sys.stderr
|
|
)
|
|
sys.exit(1)
|
|
raise e
|