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.
Files changed (46) hide show
  1. gfa_mcp-0.1.0/PKG-INFO +83 -0
  2. gfa_mcp-0.1.0/README.md +59 -0
  3. gfa_mcp-0.1.0/gfa_mcp/__init__.py +16 -0
  4. gfa_mcp-0.1.0/gfa_mcp/_version.py +7 -0
  5. gfa_mcp-0.1.0/gfa_mcp/auth.py +69 -0
  6. gfa_mcp-0.1.0/gfa_mcp/cli.py +239 -0
  7. gfa_mcp-0.1.0/gfa_mcp/dispatcher.py +241 -0
  8. gfa_mcp-0.1.0/gfa_mcp/errors.py +63 -0
  9. gfa_mcp-0.1.0/gfa_mcp/handlers/__init__.py +53 -0
  10. gfa_mcp-0.1.0/gfa_mcp/handlers/_util.py +65 -0
  11. gfa_mcp-0.1.0/gfa_mcp/handlers/bisect.py +46 -0
  12. gfa_mcp-0.1.0/gfa_mcp/handlers/commit.py +53 -0
  13. gfa_mcp-0.1.0/gfa_mcp/handlers/conflict_surface.py +21 -0
  14. gfa_mcp-0.1.0/gfa_mcp/handlers/diverged.py +24 -0
  15. gfa_mcp-0.1.0/gfa_mcp/handlers/fork.py +26 -0
  16. gfa_mcp-0.1.0/gfa_mcp/handlers/hint.py +22 -0
  17. gfa_mcp-0.1.0/gfa_mcp/handlers/list_tree.py +22 -0
  18. gfa_mcp-0.1.0/gfa_mcp/handlers/partial_clone.py +48 -0
  19. gfa_mcp-0.1.0/gfa_mcp/handlers/profile.py +22 -0
  20. gfa_mcp-0.1.0/gfa_mcp/handlers/read_file.py +54 -0
  21. gfa_mcp-0.1.0/gfa_mcp/handlers/types.py +30 -0
  22. gfa_mcp-0.1.0/gfa_mcp/handlers/workspace.py +59 -0
  23. gfa_mcp-0.1.0/gfa_mcp/log.py +46 -0
  24. gfa_mcp-0.1.0/gfa_mcp/session.py +294 -0
  25. gfa_mcp-0.1.0/gfa_mcp/tool_schemas.py +322 -0
  26. gfa_mcp-0.1.0/gfa_mcp/tools.toml +180 -0
  27. gfa_mcp-0.1.0/gfa_mcp/tools_loader.py +83 -0
  28. gfa_mcp-0.1.0/gfa_mcp/transport/__init__.py +7 -0
  29. gfa_mcp-0.1.0/gfa_mcp/transport/http.py +148 -0
  30. gfa_mcp-0.1.0/gfa_mcp/transport/stdio.py +47 -0
  31. gfa_mcp-0.1.0/gfa_mcp/transport/unix.py +88 -0
  32. gfa_mcp-0.1.0/gfa_mcp.egg-info/PKG-INFO +83 -0
  33. gfa_mcp-0.1.0/gfa_mcp.egg-info/SOURCES.txt +44 -0
  34. gfa_mcp-0.1.0/gfa_mcp.egg-info/dependency_links.txt +1 -0
  35. gfa_mcp-0.1.0/gfa_mcp.egg-info/entry_points.txt +2 -0
  36. gfa_mcp-0.1.0/gfa_mcp.egg-info/requires.txt +6 -0
  37. gfa_mcp-0.1.0/gfa_mcp.egg-info/top_level.txt +1 -0
  38. gfa_mcp-0.1.0/pyproject.toml +52 -0
  39. gfa_mcp-0.1.0/setup.cfg +4 -0
  40. gfa_mcp-0.1.0/tests/test_cli.py +51 -0
  41. gfa_mcp-0.1.0/tests/test_dispatcher.py +229 -0
  42. gfa_mcp-0.1.0/tests/test_session.py +108 -0
  43. gfa_mcp-0.1.0/tests/test_tool_schemas.py +51 -0
  44. gfa_mcp-0.1.0/tests/test_tools_loader.py +50 -0
  45. gfa_mcp-0.1.0/tests/test_transport_http.py +38 -0
  46. 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.
@@ -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())