gfa-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gfa_mcp-0.1.0/PKG-INFO +83 -0
- gfa_mcp-0.1.0/README.md +59 -0
- gfa_mcp-0.1.0/gfa_mcp/__init__.py +16 -0
- gfa_mcp-0.1.0/gfa_mcp/_version.py +7 -0
- gfa_mcp-0.1.0/gfa_mcp/auth.py +69 -0
- gfa_mcp-0.1.0/gfa_mcp/cli.py +239 -0
- gfa_mcp-0.1.0/gfa_mcp/dispatcher.py +241 -0
- gfa_mcp-0.1.0/gfa_mcp/errors.py +63 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/__init__.py +53 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/_util.py +65 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/bisect.py +46 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/commit.py +53 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/conflict_surface.py +21 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/diverged.py +24 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/fork.py +26 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/hint.py +22 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/list_tree.py +22 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/partial_clone.py +48 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/profile.py +22 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/read_file.py +54 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/types.py +30 -0
- gfa_mcp-0.1.0/gfa_mcp/handlers/workspace.py +59 -0
- gfa_mcp-0.1.0/gfa_mcp/log.py +46 -0
- gfa_mcp-0.1.0/gfa_mcp/session.py +294 -0
- gfa_mcp-0.1.0/gfa_mcp/tool_schemas.py +322 -0
- gfa_mcp-0.1.0/gfa_mcp/tools.toml +180 -0
- gfa_mcp-0.1.0/gfa_mcp/tools_loader.py +83 -0
- gfa_mcp-0.1.0/gfa_mcp/transport/__init__.py +7 -0
- gfa_mcp-0.1.0/gfa_mcp/transport/http.py +148 -0
- gfa_mcp-0.1.0/gfa_mcp/transport/stdio.py +47 -0
- gfa_mcp-0.1.0/gfa_mcp/transport/unix.py +88 -0
- gfa_mcp-0.1.0/gfa_mcp.egg-info/PKG-INFO +83 -0
- gfa_mcp-0.1.0/gfa_mcp.egg-info/SOURCES.txt +44 -0
- gfa_mcp-0.1.0/gfa_mcp.egg-info/dependency_links.txt +1 -0
- gfa_mcp-0.1.0/gfa_mcp.egg-info/entry_points.txt +2 -0
- gfa_mcp-0.1.0/gfa_mcp.egg-info/requires.txt +6 -0
- gfa_mcp-0.1.0/gfa_mcp.egg-info/top_level.txt +1 -0
- gfa_mcp-0.1.0/pyproject.toml +52 -0
- gfa_mcp-0.1.0/setup.cfg +4 -0
- gfa_mcp-0.1.0/tests/test_cli.py +51 -0
- gfa_mcp-0.1.0/tests/test_dispatcher.py +229 -0
- gfa_mcp-0.1.0/tests/test_session.py +108 -0
- gfa_mcp-0.1.0/tests/test_tool_schemas.py +51 -0
- gfa_mcp-0.1.0/tests/test_tools_loader.py +50 -0
- gfa_mcp-0.1.0/tests/test_transport_http.py +38 -0
- gfa_mcp-0.1.0/tests/test_transport_stdio.py +43 -0
gfa_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gfa-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server wrapping the gfa Python SDK; exposes 14 tools to MCP-aware agents.
|
|
5
|
+
Author: gfa contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://gitlab.com/kerusu/gfa
|
|
8
|
+
Project-URL: Repository, https://gitlab.com/kerusu/gfa
|
|
9
|
+
Keywords: gfa,git,agent,mcp,model-context-protocol
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: gfa-sdk>=0.1.0
|
|
20
|
+
Requires-Dist: PyYAML>=6
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
|
|
24
|
+
|
|
25
|
+
# gfa-mcp
|
|
26
|
+
|
|
27
|
+
Model Context Protocol server wrapping the gfa Python SDK.
|
|
28
|
+
|
|
29
|
+
`gfa-mcp` is a sidecar that exposes 14 gfa primitives as MCP tools so
|
|
30
|
+
off-the-shelf agent platforms (Claude Code, Cursor, Codex Agent, Cline,
|
|
31
|
+
Continue.dev) can call them without bespoke per-platform integration.
|
|
32
|
+
|
|
33
|
+
The MCP server is a thin adapter — every tool maps 1:1 to an SDK method.
|
|
34
|
+
Routing, caching, and threshold heuristics live in the SDK
|
|
35
|
+
(`gfa-sdk`), not here. See `docs/architecture/sdk-mcp.md` in the gfa
|
|
36
|
+
repo for the full design.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
pip install gfa-mcp
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
# Stdio (agent platform spawns gfa-mcp as a subprocess)
|
|
45
|
+
gfa-mcp --stdio --endpoint https://gfa.example.com --token "$GFA_JWT"
|
|
46
|
+
|
|
47
|
+
# HTTP (long-running, multiple agents on the same host)
|
|
48
|
+
gfa-mcp --port 8765 --endpoint https://gfa.example.com --key ~/.gfa/agent.pem
|
|
49
|
+
|
|
50
|
+
# Inspect the tool list without running the server
|
|
51
|
+
gfa-mcp --print-tools
|
|
52
|
+
|
|
53
|
+
## Claude Code wiring
|
|
54
|
+
|
|
55
|
+
`~/.claude-code/mcp.json`:
|
|
56
|
+
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"gfa": {
|
|
60
|
+
"command": "gfa-mcp",
|
|
61
|
+
"args": [
|
|
62
|
+
"--stdio",
|
|
63
|
+
"--endpoint", "https://gfa.example.com",
|
|
64
|
+
"--key", "/home/user/.gfa/agent.pem"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
Cursor and Codex Agent use a similar shape per their respective docs.
|
|
71
|
+
|
|
72
|
+
## Auth
|
|
73
|
+
|
|
74
|
+
`--token JWT` for pre-minted JWTs (simple, doesn't auto-rotate).
|
|
75
|
+
`--key PATH` for ECDSA private keys (auto-rotates short-lived tokens via
|
|
76
|
+
the SDK's `FileKeyTokenProvider`). Use `--key` for any session expected
|
|
77
|
+
to live longer than the JWT's TTL.
|
|
78
|
+
|
|
79
|
+
## Customer docs
|
|
80
|
+
|
|
81
|
+
The full agent integration guide — AGENTS.md template, system-prompt
|
|
82
|
+
snippets, per-platform config — is M-055-CUSTOMER-DOCS in the gfa
|
|
83
|
+
project backlog and ships separately from this package.
|
gfa_mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# gfa-mcp
|
|
2
|
+
|
|
3
|
+
Model Context Protocol server wrapping the gfa Python SDK.
|
|
4
|
+
|
|
5
|
+
`gfa-mcp` is a sidecar that exposes 14 gfa primitives as MCP tools so
|
|
6
|
+
off-the-shelf agent platforms (Claude Code, Cursor, Codex Agent, Cline,
|
|
7
|
+
Continue.dev) can call them without bespoke per-platform integration.
|
|
8
|
+
|
|
9
|
+
The MCP server is a thin adapter — every tool maps 1:1 to an SDK method.
|
|
10
|
+
Routing, caching, and threshold heuristics live in the SDK
|
|
11
|
+
(`gfa-sdk`), not here. See `docs/architecture/sdk-mcp.md` in the gfa
|
|
12
|
+
repo for the full design.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
pip install gfa-mcp
|
|
17
|
+
|
|
18
|
+
## Quickstart
|
|
19
|
+
|
|
20
|
+
# Stdio (agent platform spawns gfa-mcp as a subprocess)
|
|
21
|
+
gfa-mcp --stdio --endpoint https://gfa.example.com --token "$GFA_JWT"
|
|
22
|
+
|
|
23
|
+
# HTTP (long-running, multiple agents on the same host)
|
|
24
|
+
gfa-mcp --port 8765 --endpoint https://gfa.example.com --key ~/.gfa/agent.pem
|
|
25
|
+
|
|
26
|
+
# Inspect the tool list without running the server
|
|
27
|
+
gfa-mcp --print-tools
|
|
28
|
+
|
|
29
|
+
## Claude Code wiring
|
|
30
|
+
|
|
31
|
+
`~/.claude-code/mcp.json`:
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"gfa": {
|
|
36
|
+
"command": "gfa-mcp",
|
|
37
|
+
"args": [
|
|
38
|
+
"--stdio",
|
|
39
|
+
"--endpoint", "https://gfa.example.com",
|
|
40
|
+
"--key", "/home/user/.gfa/agent.pem"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Cursor and Codex Agent use a similar shape per their respective docs.
|
|
47
|
+
|
|
48
|
+
## Auth
|
|
49
|
+
|
|
50
|
+
`--token JWT` for pre-minted JWTs (simple, doesn't auto-rotate).
|
|
51
|
+
`--key PATH` for ECDSA private keys (auto-rotates short-lived tokens via
|
|
52
|
+
the SDK's `FileKeyTokenProvider`). Use `--key` for any session expected
|
|
53
|
+
to live longer than the JWT's TTL.
|
|
54
|
+
|
|
55
|
+
## Customer docs
|
|
56
|
+
|
|
57
|
+
The full agent integration guide — AGENTS.md template, system-prompt
|
|
58
|
+
snippets, per-platform config — is M-055-CUSTOMER-DOCS in the gfa
|
|
59
|
+
project backlog and ships separately from this package.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""gfa-mcp — Model Context Protocol server wrapping the gfa Python SDK.
|
|
2
|
+
|
|
3
|
+
The MCP server is a thin adapter: every tool maps 1:1 to an SDK method.
|
|
4
|
+
Routing and caching live in the SDK, not here. See
|
|
5
|
+
``design/mcp-server-architecture.md`` in the gfa repo.
|
|
6
|
+
|
|
7
|
+
Public surface used by tests:
|
|
8
|
+
|
|
9
|
+
- :class:`gfa_mcp.session.SessionState`
|
|
10
|
+
- :class:`gfa_mcp.dispatcher.Dispatcher`
|
|
11
|
+
- :func:`gfa_mcp.tools_loader.load_tools_toml`
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from gfa_mcp._version import __version__
|
|
15
|
+
|
|
16
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Package version + MCP protocol version pinned at build time."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
# MCP spec version this server speaks. Pinned per design §11 (Risks) — drift
|
|
6
|
+
# is loud, not silent. Upgrading is a single-file change.
|
|
7
|
+
MCP_PROTOCOL_VERSION = "2025-03-26"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Auth mode resolution.
|
|
2
|
+
|
|
3
|
+
Two modes per design §Auth flow:
|
|
4
|
+
|
|
5
|
+
- **Mode A — pre-configured JWT.** Either a literal JWT string (--token)
|
|
6
|
+
or a private key path used by the SDK's FileKeyTokenProvider (--key).
|
|
7
|
+
- **Mode B — pass-through.** The MCP layer reads the JWT from each
|
|
8
|
+
agent-supplied request (HTTP Authorization header or stdio param). Not
|
|
9
|
+
implemented in v1 transports — we surface a clear error when selected.
|
|
10
|
+
|
|
11
|
+
This module hands the dispatcher a ready-to-use token or TokenProvider;
|
|
12
|
+
the dispatcher constructs ``gfa.Client`` with whatever it gets.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Union
|
|
19
|
+
|
|
20
|
+
from gfa import FileKeyTokenProvider, TokenProvider
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def resolve_auth(
|
|
24
|
+
*,
|
|
25
|
+
token: str | None,
|
|
26
|
+
key: str | None,
|
|
27
|
+
auth_mode: str = "static",
|
|
28
|
+
token_ttl_hours: int = 1,
|
|
29
|
+
) -> Union[str, TokenProvider]:
|
|
30
|
+
"""Pick the right auth artifact for ``gfa.Client(token=...)``.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
token: pre-minted JWT string (mutually exclusive with ``key``).
|
|
34
|
+
key: path to ECDSA private key (mutually exclusive with ``token``).
|
|
35
|
+
auth_mode: ``static`` (default) or ``pass-through``. The latter is
|
|
36
|
+
documented but not yet wired through the transports — passing
|
|
37
|
+
it raises ``NotImplementedError`` so customers see the gap
|
|
38
|
+
instead of a silent misroute.
|
|
39
|
+
token_ttl_hours: TTL for minted tokens when ``key`` is set.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Either a JWT string (Mode A: --token) or a TokenProvider (Mode A:
|
|
43
|
+
--key).
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: neither or both of token / key supplied.
|
|
47
|
+
NotImplementedError: pass-through mode selected.
|
|
48
|
+
"""
|
|
49
|
+
if auth_mode == "pass-through":
|
|
50
|
+
raise NotImplementedError(
|
|
51
|
+
"pass-through auth (Mode B) is not yet wired through any transport. "
|
|
52
|
+
"Use --token or --key for now; per-request token injection lands in "
|
|
53
|
+
"M-055-MCP-AUTH-PASSTHROUGH (sibling backlog).",
|
|
54
|
+
)
|
|
55
|
+
if auth_mode != "static":
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"auth_mode must be 'static' or 'pass-through', got {auth_mode!r}",
|
|
58
|
+
)
|
|
59
|
+
if token and key:
|
|
60
|
+
raise ValueError("--token and --key are mutually exclusive")
|
|
61
|
+
if not token and not key:
|
|
62
|
+
raise ValueError("one of --token or --key is required (Mode A)")
|
|
63
|
+
if token:
|
|
64
|
+
return token
|
|
65
|
+
provider = FileKeyTokenProvider(
|
|
66
|
+
Path(key), # type: ignore[arg-type]
|
|
67
|
+
ttl_hours=token_ttl_hours,
|
|
68
|
+
)
|
|
69
|
+
return provider
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Command-line entry point.
|
|
2
|
+
|
|
3
|
+
``gfa-mcp`` is the installed console script; this module's :func:`main`
|
|
4
|
+
is its handler. Argument layering: CLI > env vars (``GFA_MCP_*``) > YAML
|
|
5
|
+
config > defaults.
|
|
6
|
+
|
|
7
|
+
The CLI is intentionally thin: it parses arguments, builds a Client +
|
|
8
|
+
SessionState + Dispatcher, and hands them to the chosen transport.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from gfa import Client
|
|
21
|
+
|
|
22
|
+
from gfa_mcp._version import MCP_PROTOCOL_VERSION, __version__
|
|
23
|
+
from gfa_mcp.auth import resolve_auth
|
|
24
|
+
from gfa_mcp.dispatcher import Dispatcher
|
|
25
|
+
from gfa_mcp.log import Logger
|
|
26
|
+
from gfa_mcp.session import SessionState
|
|
27
|
+
from gfa_mcp.tool_schemas import INPUT_SCHEMAS, TOOL_NAMES
|
|
28
|
+
from gfa_mcp.tools_loader import default_tools_toml_path, load_tools_toml
|
|
29
|
+
from gfa_mcp.transport.http import HttpTransport
|
|
30
|
+
from gfa_mcp.transport.stdio import StdioTransport
|
|
31
|
+
from gfa_mcp.transport.unix import UnixTransport
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
35
|
+
p = argparse.ArgumentParser(
|
|
36
|
+
prog="gfa-mcp",
|
|
37
|
+
description="gfa-mcp — Model Context Protocol server wrapping the gfa SDK",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Connection target
|
|
41
|
+
p.add_argument("--endpoint", help="gfa server URL (e.g. https://gfa.example.com)")
|
|
42
|
+
p.add_argument("--config", help="YAML config file (overrides individual flags)")
|
|
43
|
+
|
|
44
|
+
# Auth
|
|
45
|
+
p.add_argument("--token", help="Pre-minted JWT (Mode A, simple)")
|
|
46
|
+
p.add_argument("--key", help="Path to private key (Mode A, auto-mint)")
|
|
47
|
+
p.add_argument(
|
|
48
|
+
"--auth",
|
|
49
|
+
choices=["static", "pass-through"],
|
|
50
|
+
default="static",
|
|
51
|
+
help="Auth mode (default: static)",
|
|
52
|
+
)
|
|
53
|
+
p.add_argument(
|
|
54
|
+
"--token-ttl-hours",
|
|
55
|
+
type=int,
|
|
56
|
+
default=1,
|
|
57
|
+
help="TTL for minted tokens when --key is used",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Transport
|
|
61
|
+
p.add_argument("--stdio", action="store_true", help="Use stdio transport")
|
|
62
|
+
p.add_argument("--port", type=int, help="Bind HTTP on 127.0.0.1:N")
|
|
63
|
+
p.add_argument("--socket", help="Bind Unix socket at PATH")
|
|
64
|
+
p.add_argument(
|
|
65
|
+
"--bind",
|
|
66
|
+
default="127.0.0.1",
|
|
67
|
+
help="Override 127.0.0.1 for HTTP (security: understand before changing)",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Tunables
|
|
71
|
+
p.add_argument("--blob-cache-mb", type=int, default=0)
|
|
72
|
+
p.add_argument(
|
|
73
|
+
"--hint-thresholds",
|
|
74
|
+
default="10,50,200",
|
|
75
|
+
help="Comma-separated thresholds for proactive hints",
|
|
76
|
+
)
|
|
77
|
+
p.add_argument("--partial-clone-ttl", type=int, default=3600)
|
|
78
|
+
p.add_argument("--max-partial-clones", type=int, default=10)
|
|
79
|
+
p.add_argument("--tools-toml", help="Override path to tools.toml")
|
|
80
|
+
|
|
81
|
+
# Logging
|
|
82
|
+
p.add_argument(
|
|
83
|
+
"--log-level",
|
|
84
|
+
default=os.environ.get("GFA_MCP_LOG_LEVEL", "INFO"),
|
|
85
|
+
choices=["DEBUG", "INFO", "WARN", "ERROR"],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Diagnostic
|
|
89
|
+
p.add_argument("--print-tools", action="store_true",
|
|
90
|
+
help="Dump the tool list as JSON and exit")
|
|
91
|
+
p.add_argument("--version", action="version",
|
|
92
|
+
version=f"gfa-mcp {__version__} (MCP {MCP_PROTOCOL_VERSION})")
|
|
93
|
+
return p
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _load_yaml_config(path: str) -> dict[str, Any]:
|
|
97
|
+
import yaml
|
|
98
|
+
|
|
99
|
+
with open(path, "rb") as f:
|
|
100
|
+
data = yaml.safe_load(f) or {}
|
|
101
|
+
if not isinstance(data, dict):
|
|
102
|
+
raise SystemExit(f"--config {path}: must be a YAML mapping")
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _merge_config(args: argparse.Namespace) -> argparse.Namespace:
|
|
107
|
+
"""Layer env vars + optional YAML config under the CLI args.
|
|
108
|
+
|
|
109
|
+
Precedence: CLI > env (``GFA_MCP_<FLAG>``) > YAML > argparse defaults.
|
|
110
|
+
"""
|
|
111
|
+
# YAML first (lowest priority over CLI/env)
|
|
112
|
+
if args.config:
|
|
113
|
+
cfg = _load_yaml_config(args.config)
|
|
114
|
+
if args.endpoint is None:
|
|
115
|
+
args.endpoint = cfg.get("endpoint")
|
|
116
|
+
auth = cfg.get("auth") or {}
|
|
117
|
+
if args.token is None:
|
|
118
|
+
args.token = auth.get("token")
|
|
119
|
+
if args.key is None:
|
|
120
|
+
args.key = auth.get("key")
|
|
121
|
+
if "mode" in auth and args.auth == "static":
|
|
122
|
+
args.auth = auth["mode"]
|
|
123
|
+
transport = cfg.get("transport") or {}
|
|
124
|
+
if args.port is None and "port" in transport:
|
|
125
|
+
args.port = transport["port"]
|
|
126
|
+
if args.socket is None and "socket" in transport:
|
|
127
|
+
args.socket = transport["socket"]
|
|
128
|
+
if "stdio" in transport and not args.stdio:
|
|
129
|
+
args.stdio = bool(transport["stdio"])
|
|
130
|
+
cache = cfg.get("cache") or {}
|
|
131
|
+
if not args.blob_cache_mb and "blob_mb" in cache:
|
|
132
|
+
args.blob_cache_mb = int(cache["blob_mb"])
|
|
133
|
+
hints = cfg.get("hints") or {}
|
|
134
|
+
if "thresholds" in hints and args.hint_thresholds == "10,50,200":
|
|
135
|
+
args.hint_thresholds = ",".join(str(int(x)) for x in hints["thresholds"])
|
|
136
|
+
pc = cfg.get("partial_clone") or {}
|
|
137
|
+
if "ttl_seconds" in pc and args.partial_clone_ttl == 3600:
|
|
138
|
+
args.partial_clone_ttl = int(pc["ttl_seconds"])
|
|
139
|
+
log = cfg.get("log") or {}
|
|
140
|
+
if "level" in log and args.log_level == "INFO":
|
|
141
|
+
args.log_level = log["level"]
|
|
142
|
+
|
|
143
|
+
# Env vars (higher than YAML, lower than CLI which is already set)
|
|
144
|
+
def _env(name: str) -> str | None:
|
|
145
|
+
return os.environ.get(name)
|
|
146
|
+
|
|
147
|
+
if args.endpoint is None:
|
|
148
|
+
args.endpoint = _env("GFA_MCP_ENDPOINT")
|
|
149
|
+
if args.token is None:
|
|
150
|
+
args.token = _env("GFA_MCP_TOKEN")
|
|
151
|
+
if args.key is None:
|
|
152
|
+
args.key = _env("GFA_MCP_KEY")
|
|
153
|
+
return args
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main(argv: list[str] | None = None) -> int:
|
|
157
|
+
"""Build and run a gfa-mcp server. Returns process exit code."""
|
|
158
|
+
parser = build_parser()
|
|
159
|
+
args = parser.parse_args(argv)
|
|
160
|
+
args = _merge_config(args)
|
|
161
|
+
|
|
162
|
+
log = Logger(args.log_level)
|
|
163
|
+
|
|
164
|
+
# Tool descriptions are loaded from tools.toml; this also cross-checks
|
|
165
|
+
# against the schema registry and fails fast if anything's missing.
|
|
166
|
+
tools_toml_path = args.tools_toml or str(default_tools_toml_path())
|
|
167
|
+
try:
|
|
168
|
+
descriptions = load_tools_toml(tools_toml_path)
|
|
169
|
+
except Exception as e: # noqa: BLE001
|
|
170
|
+
log.error("tools.toml load failed", path=tools_toml_path, error=str(e))
|
|
171
|
+
print(f"gfa-mcp: tools.toml load failed: {e}", file=sys.stderr)
|
|
172
|
+
return 2
|
|
173
|
+
|
|
174
|
+
if args.print_tools:
|
|
175
|
+
out = [
|
|
176
|
+
{
|
|
177
|
+
"name": name,
|
|
178
|
+
"description": descriptions[name],
|
|
179
|
+
"inputSchema": INPUT_SCHEMAS[name],
|
|
180
|
+
}
|
|
181
|
+
for name in TOOL_NAMES
|
|
182
|
+
]
|
|
183
|
+
print(json.dumps({"tools": out}, indent=2, sort_keys=True))
|
|
184
|
+
return 0
|
|
185
|
+
|
|
186
|
+
if not args.endpoint:
|
|
187
|
+
print("gfa-mcp: --endpoint is required", file=sys.stderr)
|
|
188
|
+
return 2
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
auth = resolve_auth(
|
|
192
|
+
token=args.token,
|
|
193
|
+
key=args.key,
|
|
194
|
+
auth_mode=args.auth,
|
|
195
|
+
token_ttl_hours=args.token_ttl_hours,
|
|
196
|
+
)
|
|
197
|
+
except (ValueError, NotImplementedError) as e:
|
|
198
|
+
print(f"gfa-mcp: auth resolution failed: {e}", file=sys.stderr)
|
|
199
|
+
return 2
|
|
200
|
+
|
|
201
|
+
client = Client(endpoint=args.endpoint, token=auth)
|
|
202
|
+
|
|
203
|
+
hint_thresholds = tuple(
|
|
204
|
+
int(x) for x in str(args.hint_thresholds).split(",") if x.strip()
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
session = SessionState(
|
|
208
|
+
client=client,
|
|
209
|
+
log=log,
|
|
210
|
+
hint_thresholds=hint_thresholds,
|
|
211
|
+
partial_clone_ttl_seconds=args.partial_clone_ttl,
|
|
212
|
+
max_partial_clones=args.max_partial_clones,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
dispatcher = Dispatcher(
|
|
216
|
+
session=session,
|
|
217
|
+
tool_descriptions=descriptions,
|
|
218
|
+
log=log,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Transport selection: explicit --socket > --port > --stdio > default(stdio)
|
|
222
|
+
try:
|
|
223
|
+
if args.socket:
|
|
224
|
+
UnixTransport(dispatcher, log, socket_path=args.socket).serve()
|
|
225
|
+
elif args.port is not None:
|
|
226
|
+
if args.bind != "127.0.0.1":
|
|
227
|
+
log.warn("binding non-loopback — no auth on the wire",
|
|
228
|
+
bind=args.bind, port=args.port)
|
|
229
|
+
HttpTransport(dispatcher, log, host=args.bind, port=args.port).serve()
|
|
230
|
+
else:
|
|
231
|
+
StdioTransport(dispatcher, log).serve()
|
|
232
|
+
finally:
|
|
233
|
+
session.close()
|
|
234
|
+
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
if __name__ == "__main__":
|
|
239
|
+
raise SystemExit(main())
|