gitcode-api 1.2.4__py3-none-any.whl → 1.2.5__py3-none-any.whl
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.
- gitcode_api/_cli_banner.py +1 -1
- gitcode_api/cli.py +67 -9
- gitcode_api/llm/__init__.py +49 -0
- gitcode_api/llm/_tool.py +368 -0
- gitcode_api/llm/mcp.py +93 -0
- gitcode_api/llm/openai.py +56 -0
- gitcode_api/py.typed +1 -0
- gitcode_api/version.txt +1 -1
- gitcode_api-1.2.5.dist-info/METADATA +371 -0
- {gitcode_api-1.2.4.dist-info → gitcode_api-1.2.5.dist-info}/RECORD +14 -9
- gitcode_api-1.2.4.dist-info/METADATA +0 -237
- {gitcode_api-1.2.4.dist-info → gitcode_api-1.2.5.dist-info}/WHEEL +0 -0
- {gitcode_api-1.2.4.dist-info → gitcode_api-1.2.5.dist-info}/entry_points.txt +0 -0
- {gitcode_api-1.2.4.dist-info → gitcode_api-1.2.5.dist-info}/licenses/LICENSE +0 -0
- {gitcode_api-1.2.4.dist-info → gitcode_api-1.2.5.dist-info}/top_level.txt +0 -0
gitcode_api/_cli_banner.py
CHANGED
gitcode_api/cli.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Command-line interface for the GitCode SDK."""
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import asyncio
|
|
4
5
|
import inspect
|
|
5
6
|
import json
|
|
6
7
|
import re
|
|
@@ -73,8 +74,10 @@ def _argument_kwargs(parameter: inspect.Parameter) -> dict[str, Any]:
|
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
def _first_doc_line(obj: Any) -> str:
|
|
76
|
-
doc = inspect.getdoc(obj)
|
|
77
|
-
|
|
77
|
+
doc = inspect.getdoc(obj)
|
|
78
|
+
if doc is not None:
|
|
79
|
+
doc = doc.removeprefix("Synchronous ").capitalize()
|
|
80
|
+
for line in (doc or "").splitlines():
|
|
78
81
|
stripped = line.strip()
|
|
79
82
|
if stripped:
|
|
80
83
|
return _plain_cli_inline(stripped)
|
|
@@ -190,6 +193,22 @@ def _root_banner() -> str:
|
|
|
190
193
|
return "Connection and defaults are documented on each method's help: %(prog)s RESOURCE METHOD -h."
|
|
191
194
|
|
|
192
195
|
|
|
196
|
+
def _serve_parser() -> argparse.ArgumentParser:
|
|
197
|
+
"""Parser with options for starting the bundled MCP server."""
|
|
198
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
199
|
+
parser.add_argument("--name", default="GitCode API", help="MCP server name.")
|
|
200
|
+
parser.add_argument("--api-key", help=f"GitCode access token. Defaults to {DEFAULT_TOKEN_ENV}.")
|
|
201
|
+
parser.add_argument("--owner", help="Default repository owner.")
|
|
202
|
+
parser.add_argument("--repo", help="Default repository name.")
|
|
203
|
+
parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Base URL for the REST API.")
|
|
204
|
+
parser.add_argument("--timeout", type=float, default=None, help="Request timeout in seconds.")
|
|
205
|
+
parser.add_argument("--transport", default="stdio", help="FastMCP transport to run, such as stdio or http.")
|
|
206
|
+
parser.add_argument("--host", default=None, help="Host for HTTP-based transports.")
|
|
207
|
+
parser.add_argument("--port", type=int, default=None, help="Port for HTTP-based transports.")
|
|
208
|
+
parser.add_argument("--path", default=None, help="Path for HTTP-based transports.")
|
|
209
|
+
return parser
|
|
210
|
+
|
|
211
|
+
|
|
193
212
|
def build_parser() -> argparse.ArgumentParser:
|
|
194
213
|
"""Build main parser."""
|
|
195
214
|
common = _invocation_parent_parser()
|
|
@@ -221,13 +240,24 @@ Each method -h opens with resource.method_signature("<name>") from the Python SD
|
|
|
221
240
|
dest="resource",
|
|
222
241
|
required=True,
|
|
223
242
|
metavar="RESOURCE",
|
|
224
|
-
title="
|
|
225
|
-
description="Pick a resource group (same attribute names as on GitCode, e.g. pulls, repos).",
|
|
243
|
+
title="commands",
|
|
244
|
+
description="Pick a command or resource group (same attribute names as on GitCode, e.g. pulls, repos).",
|
|
226
245
|
)
|
|
246
|
+
serve_parser = resource_parsers.add_parser(
|
|
247
|
+
"serve",
|
|
248
|
+
help="Start the bundled GitCode MCP server.",
|
|
249
|
+
description="Start a FastMCP server exposing the gitcode_api_tool.",
|
|
250
|
+
formatter_class=_CLIHelpFormatter,
|
|
251
|
+
parents=[_serve_parser()],
|
|
252
|
+
)
|
|
253
|
+
serve_parser.set_defaults(command="serve")
|
|
254
|
+
|
|
227
255
|
for resource_name, resource_type in _resource_types().items():
|
|
228
256
|
resource = getattr(client, resource_name)
|
|
229
|
-
class_doc = inspect.getdoc(resource_type)
|
|
230
|
-
|
|
257
|
+
class_doc = inspect.getdoc(resource_type)
|
|
258
|
+
if class_doc is not None:
|
|
259
|
+
class_doc = class_doc.removeprefix("Synchronous ").capitalize()
|
|
260
|
+
class_lead = (class_doc or "").split("\n\n", maxsplit=1)[0].strip() if class_doc else ""
|
|
231
261
|
class_lead = _plain_cli_inline(class_lead) if class_lead else ""
|
|
232
262
|
resource_desc_parts = [
|
|
233
263
|
class_lead or _first_doc_line(resource_type),
|
|
@@ -254,8 +284,10 @@ Each method -h opens with resource.method_signature("<name>") from the Python SD
|
|
|
254
284
|
for method_name in resource.methods:
|
|
255
285
|
method = getattr(resource, method_name)
|
|
256
286
|
sig_line = resource.method_signature(method_name)
|
|
257
|
-
doc = inspect.getdoc(method)
|
|
258
|
-
|
|
287
|
+
doc = inspect.getdoc(method)
|
|
288
|
+
if doc is not None:
|
|
289
|
+
doc = doc.removeprefix("Synchronous ").capitalize()
|
|
290
|
+
summary = _method_cli_summary(doc or "")
|
|
259
291
|
if summary and len(summary) > 90:
|
|
260
292
|
method_help = summary[:87] + "..."
|
|
261
293
|
else:
|
|
@@ -306,6 +338,29 @@ Each method -h opens with resource.method_signature("<name>") from the Python SD
|
|
|
306
338
|
return parser
|
|
307
339
|
|
|
308
340
|
|
|
341
|
+
def _run_mcp_server(args: argparse.Namespace) -> int:
|
|
342
|
+
"""Start the bundled FastMCP server."""
|
|
343
|
+
from .llm import create_mcp_server
|
|
344
|
+
from .llm._tool import GitCodeLLMTool
|
|
345
|
+
|
|
346
|
+
tool = GitCodeLLMTool(
|
|
347
|
+
api_key=args.api_key,
|
|
348
|
+
owner=args.owner,
|
|
349
|
+
repo=args.repo,
|
|
350
|
+
base_url=args.base_url,
|
|
351
|
+
timeout=args.timeout,
|
|
352
|
+
)
|
|
353
|
+
server = create_mcp_server(name=args.name, tool=tool)
|
|
354
|
+
run_kwargs: dict[str, Any] = {"transport": args.transport}
|
|
355
|
+
for key in ("host", "port", "path"):
|
|
356
|
+
value = getattr(args, key)
|
|
357
|
+
if value is not None:
|
|
358
|
+
run_kwargs[key] = value
|
|
359
|
+
|
|
360
|
+
server.run(**run_kwargs)
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
|
|
309
364
|
def _collect_kwargs(args: argparse.Namespace, method: Any) -> dict[str, Any]:
|
|
310
365
|
signature = inspect.signature(method)
|
|
311
366
|
kwargs: dict[str, Any] = {}
|
|
@@ -350,6 +405,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
350
405
|
args = parser.parse_args(effective)
|
|
351
406
|
|
|
352
407
|
try:
|
|
408
|
+
if getattr(args, "command", None) == "serve":
|
|
409
|
+
return _run_mcp_server(args)
|
|
410
|
+
|
|
353
411
|
with GitCode(
|
|
354
412
|
api_key=args.api_key,
|
|
355
413
|
owner=args.owner,
|
|
@@ -360,7 +418,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
360
418
|
resource = getattr(client, args.resource_name)
|
|
361
419
|
method = getattr(resource, args.method_name)
|
|
362
420
|
result = method(**_collect_kwargs(args, method))
|
|
363
|
-
except (GitCodeError, OSError, TypeError, ValueError) as exc: # pragma: no cover - integration style
|
|
421
|
+
except (GitCodeError, ImportError, OSError, TypeError, ValueError) as exc: # pragma: no cover - integration style
|
|
364
422
|
print(f"error: {exc}", file=sys.stderr)
|
|
365
423
|
return 1
|
|
366
424
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""LLM tool adapters for the GitCode SDK."""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Dict
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .mcp import GitCodeMCP, create_mcp_gitcode_api_tool, create_mcp_server, register_mcp_gitcode_api_tool
|
|
8
|
+
from .openai import GitCodeOpenAITool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_IMPORT_MAP = {
|
|
12
|
+
"GitCodeMCP": ".mcp",
|
|
13
|
+
"create_mcp_gitcode_api_tool": ".mcp",
|
|
14
|
+
"create_mcp_server": ".mcp",
|
|
15
|
+
"register_mcp_gitcode_api_tool": ".mcp",
|
|
16
|
+
"GitCodeOpenAITool": ".openai",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_IMPORT_CACHE: Dict[str, Any] = {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def __getattr__(name: str) -> object:
|
|
23
|
+
"""Lazily import LLM adapter class / helper functions."""
|
|
24
|
+
value = _IMPORT_CACHE.get(name)
|
|
25
|
+
if value is not None:
|
|
26
|
+
return value
|
|
27
|
+
try:
|
|
28
|
+
module_name = _IMPORT_MAP[name]
|
|
29
|
+
except KeyError as exc:
|
|
30
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from exc
|
|
31
|
+
|
|
32
|
+
module = import_module(module_name, __name__)
|
|
33
|
+
for attr in module.__all__:
|
|
34
|
+
_IMPORT_CACHE[attr] = getattr(module, attr)
|
|
35
|
+
return _IMPORT_CACHE[name]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def __dir__() -> list[str]:
|
|
39
|
+
"""Return module attributes."""
|
|
40
|
+
return __all__
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"GitCodeOpenAITool",
|
|
45
|
+
"GitCodeMCP",
|
|
46
|
+
"create_mcp_gitcode_api_tool",
|
|
47
|
+
"create_mcp_server",
|
|
48
|
+
"register_mcp_gitcode_api_tool",
|
|
49
|
+
]
|
gitcode_api/llm/_tool.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Shared GitCode API tool logic for LLM integrations."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
10
|
+
|
|
11
|
+
from gitcode_api import (
|
|
12
|
+
AsyncGitCode,
|
|
13
|
+
GitCode,
|
|
14
|
+
GitCodeConfigurationError,
|
|
15
|
+
GitCodeError,
|
|
16
|
+
GitCodeHTTPStatusError,
|
|
17
|
+
)
|
|
18
|
+
from gitcode_api._base_client import DEFAULT_BASE_URL, DEFAULT_TOKEN_ENV
|
|
19
|
+
from gitcode_api.resources._shared import AsyncResource, SyncResource
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
OP_TYPES = frozenset(
|
|
24
|
+
name
|
|
25
|
+
for name, annotation in GitCode.__annotations__.items()
|
|
26
|
+
if inspect.isclass(annotation) and issubclass(annotation, SyncResource)
|
|
27
|
+
)
|
|
28
|
+
OP_TYPE_ENUM = sorted(OP_TYPES)
|
|
29
|
+
TOOL_NAME = "gitcode_api_tool"
|
|
30
|
+
TOOL_DESCRIPTION = (
|
|
31
|
+
"Call the GitCode REST API through the gitcode-api SDK. Use op_type to choose a client resource group, "
|
|
32
|
+
"action as the resource method name, and params as the keyword arguments for that method. Set help=true to "
|
|
33
|
+
"inspect available resource methods or a target method signature without sending an API request."
|
|
34
|
+
)
|
|
35
|
+
TOOL_PARAMETERS = {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"op_type": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"enum": OP_TYPE_ENUM,
|
|
41
|
+
"description": "SDK resource group matching client.<op_type>, such as repos, issues, or pulls.",
|
|
42
|
+
},
|
|
43
|
+
"action": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Method name on the selected resource, such as get or list. Omit for method help.",
|
|
46
|
+
},
|
|
47
|
+
"params": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"description": "Keyword arguments passed to the SDK method. Omitted or null is treated as {}.",
|
|
50
|
+
},
|
|
51
|
+
"help": {
|
|
52
|
+
"type": "boolean",
|
|
53
|
+
"description": "When true, return dynamic SDK help instead of sending a request.",
|
|
54
|
+
"default": False,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
"required": ["op_type"],
|
|
58
|
+
}
|
|
59
|
+
HELP_MESSAGE_TEMPLATE = """This is a help message, pass in help=false for normal usage.
|
|
60
|
+
{header}
|
|
61
|
+
Resource: {op_type}
|
|
62
|
+
|
|
63
|
+
{actions_block}
|
|
64
|
+
{footer}""".strip()
|
|
65
|
+
_INTROSPECT_API_KEY = "introspection-only"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def error_dict(message: str, **extra: Any) -> Dict[str, Any]:
|
|
69
|
+
"""Return a normalized tool error payload."""
|
|
70
|
+
out = {"error": True, "message": message}
|
|
71
|
+
out.update(extra)
|
|
72
|
+
return out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def serialize(value: Any) -> Any:
|
|
76
|
+
"""Convert SDK responses into JSON-serializable values for tool clients."""
|
|
77
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
78
|
+
return value
|
|
79
|
+
if isinstance(value, bytes):
|
|
80
|
+
return {
|
|
81
|
+
"encoding": "base64",
|
|
82
|
+
"data": base64.b64encode(value).decode("ascii"),
|
|
83
|
+
}
|
|
84
|
+
if isinstance(value, dict):
|
|
85
|
+
return {key: serialize(item) for key, item in value.items()}
|
|
86
|
+
if isinstance(value, (list, tuple)):
|
|
87
|
+
return [serialize(item) for item in value]
|
|
88
|
+
to_dict = getattr(value, "to_dict", None)
|
|
89
|
+
if callable(to_dict):
|
|
90
|
+
return serialize(to_dict())
|
|
91
|
+
if isinstance(value, Mapping):
|
|
92
|
+
return {key: serialize(item) for key, item in dict(value).items()}
|
|
93
|
+
return str(value)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _actions_block(resource: Any) -> str:
|
|
97
|
+
lines = [resource.method_signature(name) for name in resource.methods]
|
|
98
|
+
return "\n".join(lines) if lines else "(no public methods)"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _format_help(*, op_type: str, resource: Any, header: str, footer: str = "") -> str:
|
|
102
|
+
block = _actions_block(resource) if resource is not None else ""
|
|
103
|
+
return HELP_MESSAGE_TEMPLATE.format(
|
|
104
|
+
header=header,
|
|
105
|
+
op_type=op_type,
|
|
106
|
+
actions_block=block,
|
|
107
|
+
footer=footer,
|
|
108
|
+
).strip()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@lru_cache(maxsize=1)
|
|
112
|
+
def introspection_client() -> GitCode:
|
|
113
|
+
"""Return a no-network client used only to inspect resource signatures."""
|
|
114
|
+
return GitCode(api_key=_INTROSPECT_API_KEY)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@lru_cache(maxsize=None)
|
|
118
|
+
def resource_for_op_type(op_type: str) -> SyncResource:
|
|
119
|
+
"""Return the sync resource used for signature and method discovery."""
|
|
120
|
+
return getattr(introspection_client(), op_type)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _format_action_help(*, op_type: str, resource: Any, action: str) -> str:
|
|
124
|
+
try:
|
|
125
|
+
sig_line = resource.method_signature(action)
|
|
126
|
+
except (TypeError, ValueError, AttributeError):
|
|
127
|
+
sig_line = f"{action}(...)"
|
|
128
|
+
header = f"Target signature:\n{sig_line}"
|
|
129
|
+
return _format_help(op_type=op_type, resource=None, header=header)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def validate_call_kwargs(
|
|
133
|
+
fn: Callable[..., Any], params: Optional[Dict[str, Any]]
|
|
134
|
+
) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
135
|
+
"""Return validated keyword arguments and an optional error message."""
|
|
136
|
+
raw = dict(params) if params else {}
|
|
137
|
+
try:
|
|
138
|
+
sig = inspect.signature(fn)
|
|
139
|
+
except (TypeError, ValueError) as exc:
|
|
140
|
+
return None, f"Cannot inspect signature: {exc}"
|
|
141
|
+
|
|
142
|
+
allowed = set()
|
|
143
|
+
required = []
|
|
144
|
+
accepts_var_kwargs = False
|
|
145
|
+
for name, param in sig.parameters.items():
|
|
146
|
+
if param.kind is inspect.Parameter.VAR_POSITIONAL:
|
|
147
|
+
continue
|
|
148
|
+
if param.kind is inspect.Parameter.VAR_KEYWORD:
|
|
149
|
+
accepts_var_kwargs = True
|
|
150
|
+
continue
|
|
151
|
+
allowed.add(name)
|
|
152
|
+
if param.default is inspect.Parameter.empty:
|
|
153
|
+
required.append(name)
|
|
154
|
+
|
|
155
|
+
unknown = sorted(key for key in raw if key not in allowed)
|
|
156
|
+
if unknown and not accepts_var_kwargs:
|
|
157
|
+
return None, f"Unknown parameter(s): {', '.join(unknown)}. Allowed: {', '.join(sorted(allowed))}"
|
|
158
|
+
|
|
159
|
+
missing = [key for key in required if key not in raw]
|
|
160
|
+
if missing:
|
|
161
|
+
return None, f"Missing required parameter(s): {', '.join(missing)}"
|
|
162
|
+
|
|
163
|
+
if accepts_var_kwargs:
|
|
164
|
+
return raw, None
|
|
165
|
+
return {key: value for key, value in raw.items() if key in allowed}, None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class GitCodeLLMTool:
|
|
169
|
+
"""Callable GitCode API tool shared by OpenAI and MCP adapters.
|
|
170
|
+
|
|
171
|
+
:param client: Optional synchronous GitCode client.
|
|
172
|
+
:param async_client: Optional asynchronous GitCode client.
|
|
173
|
+
:param api_key: Personal access token used when clients are not supplied.
|
|
174
|
+
:param owner: Default repository owner for generated clients.
|
|
175
|
+
:param repo: Default repository name for generated clients.
|
|
176
|
+
:param base_url: Base URL for generated clients.
|
|
177
|
+
:param timeout: Request timeout for generated clients.
|
|
178
|
+
:param decrypt: Optional decryption function for encrypted access tokens.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def __init__(
|
|
182
|
+
self,
|
|
183
|
+
*,
|
|
184
|
+
client: Optional[GitCode] = None,
|
|
185
|
+
async_client: Optional[AsyncGitCode] = None,
|
|
186
|
+
api_key: Optional[str] = None,
|
|
187
|
+
owner: Optional[str] = None,
|
|
188
|
+
repo: Optional[str] = None,
|
|
189
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
190
|
+
timeout: Optional[float] = None,
|
|
191
|
+
decrypt: Optional[Callable] = None,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Create a reusable tool with lazy sync and async clients."""
|
|
194
|
+
if not (api_key or os.getenv(DEFAULT_TOKEN_ENV)):
|
|
195
|
+
raise GitCodeConfigurationError("No API key provided. Pass api_key=... or set GITCODE_ACCESS_TOKEN.")
|
|
196
|
+
self._client = client
|
|
197
|
+
self._async_client = async_client
|
|
198
|
+
self._client_kwargs = {
|
|
199
|
+
"api_key": api_key,
|
|
200
|
+
"owner": owner,
|
|
201
|
+
"repo": repo,
|
|
202
|
+
"base_url": base_url,
|
|
203
|
+
"timeout": timeout,
|
|
204
|
+
"decrypt": decrypt,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def client(self) -> GitCode:
|
|
209
|
+
"""Return the synchronous client, creating it lazily when needed."""
|
|
210
|
+
if self._client is None:
|
|
211
|
+
self._client = GitCode(**self._client_kwargs) # type: ignore[arg-type]
|
|
212
|
+
return self._client
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def async_client(self) -> AsyncGitCode:
|
|
216
|
+
"""Return the asynchronous client, creating it lazily when needed."""
|
|
217
|
+
if self._async_client is None:
|
|
218
|
+
self._async_client = AsyncGitCode(**self._client_kwargs) # type: ignore[arg-type]
|
|
219
|
+
return self._async_client
|
|
220
|
+
|
|
221
|
+
def __call__(
|
|
222
|
+
self,
|
|
223
|
+
op_type: str,
|
|
224
|
+
action: str = "",
|
|
225
|
+
params: Optional[Dict[str, Any]] = None,
|
|
226
|
+
help: bool = False,
|
|
227
|
+
**kwargs,
|
|
228
|
+
) -> Any:
|
|
229
|
+
"""Invoke a synchronous GitCode SDK method through the tool contract."""
|
|
230
|
+
if kwargs:
|
|
231
|
+
return error_dict('Invalid tool invoke, API parameters should go into "params"')
|
|
232
|
+
return self._invoke(op_type=op_type, action=action, params=params, help=help)
|
|
233
|
+
|
|
234
|
+
async def __async_call__(
|
|
235
|
+
self,
|
|
236
|
+
op_type: str,
|
|
237
|
+
action: str = "",
|
|
238
|
+
params: Optional[Dict[str, Any]] = None,
|
|
239
|
+
help: bool = False,
|
|
240
|
+
**kwargs,
|
|
241
|
+
) -> Any:
|
|
242
|
+
"""Invoke an asynchronous GitCode SDK method through the tool contract."""
|
|
243
|
+
if kwargs:
|
|
244
|
+
return error_dict('Invalid tool invoke, API parameters should go into "params"')
|
|
245
|
+
return await self._ainvoke(op_type=op_type, action=action, params=params, help=help)
|
|
246
|
+
|
|
247
|
+
def _validate_action(
|
|
248
|
+
self,
|
|
249
|
+
*,
|
|
250
|
+
op_type: str,
|
|
251
|
+
action: str,
|
|
252
|
+
params: Optional[Dict[str, Any]],
|
|
253
|
+
help: bool,
|
|
254
|
+
) -> Tuple[Optional[str], Optional[Dict[str, Any]], Any]:
|
|
255
|
+
if op_type not in OP_TYPES:
|
|
256
|
+
allowed = ", ".join(OP_TYPE_ENUM)
|
|
257
|
+
msg = f"Invalid op_type {op_type!r}. Allowed: {allowed}"
|
|
258
|
+
if help:
|
|
259
|
+
return (
|
|
260
|
+
HELP_MESSAGE_TEMPLATE.format(
|
|
261
|
+
header=msg,
|
|
262
|
+
op_type="(invalid)",
|
|
263
|
+
actions_block="",
|
|
264
|
+
footer="",
|
|
265
|
+
).strip(),
|
|
266
|
+
None,
|
|
267
|
+
None,
|
|
268
|
+
)
|
|
269
|
+
return str(error_dict(msg, op_type=op_type)), None, None
|
|
270
|
+
|
|
271
|
+
resource = resource_for_op_type(op_type)
|
|
272
|
+
action = (action or "").strip()
|
|
273
|
+
if not action:
|
|
274
|
+
header = "Specify non-empty action to invoke a method. Available methods:"
|
|
275
|
+
return _format_help(op_type=op_type, resource=resource, header=header), None, None
|
|
276
|
+
|
|
277
|
+
if action not in resource.methods:
|
|
278
|
+
msg = f"Unknown action {action!r} for op_type={op_type!r}."
|
|
279
|
+
if help:
|
|
280
|
+
footer = f"\n({msg})"
|
|
281
|
+
return (
|
|
282
|
+
_format_help(op_type=op_type, resource=resource, header="Available methods:", footer=footer),
|
|
283
|
+
None,
|
|
284
|
+
None,
|
|
285
|
+
)
|
|
286
|
+
return str(error_dict(msg, op_type=op_type, action=action)), None, None
|
|
287
|
+
|
|
288
|
+
if help:
|
|
289
|
+
return _format_action_help(op_type=op_type, resource=resource, action=action), None, None
|
|
290
|
+
|
|
291
|
+
method = getattr(resource, action)
|
|
292
|
+
call_kwargs, verr = validate_call_kwargs(method, params)
|
|
293
|
+
if verr is not None:
|
|
294
|
+
if help:
|
|
295
|
+
try:
|
|
296
|
+
sig_line = resource.method_signature(action)
|
|
297
|
+
except (TypeError, ValueError, AttributeError):
|
|
298
|
+
sig_line = f"{action}(...)"
|
|
299
|
+
header = f"{verr}\n\nTarget signature:\n{sig_line}"
|
|
300
|
+
return _format_help(op_type=op_type, resource=None, header=header), None, None
|
|
301
|
+
return str(error_dict(verr, op_type=op_type, action=action)), None, None
|
|
302
|
+
|
|
303
|
+
return None, call_kwargs or {}, resource
|
|
304
|
+
|
|
305
|
+
def _handle_exception(self, exc: Exception, *, op_type: str, resource: Any, help: bool) -> Dict[str, Any]:
|
|
306
|
+
if isinstance(exc, GitCodeConfigurationError):
|
|
307
|
+
return error_dict(str(exc), kind="configuration")
|
|
308
|
+
if isinstance(exc, GitCodeHTTPStatusError):
|
|
309
|
+
return error_dict(
|
|
310
|
+
str(exc),
|
|
311
|
+
kind="http",
|
|
312
|
+
status_code=getattr(exc, "status_code", None),
|
|
313
|
+
payload=getattr(exc, "payload", None),
|
|
314
|
+
)
|
|
315
|
+
if isinstance(exc, GitCodeError):
|
|
316
|
+
return error_dict(str(exc), kind="gitcode_api")
|
|
317
|
+
|
|
318
|
+
logger.exception("gitcode_api_tool: unexpected error")
|
|
319
|
+
return error_dict(f"Unexpected error: {exc}", kind="unexpected")
|
|
320
|
+
|
|
321
|
+
def _invoke(
|
|
322
|
+
self,
|
|
323
|
+
*,
|
|
324
|
+
op_type: str,
|
|
325
|
+
action: str,
|
|
326
|
+
params: Optional[Dict[str, Any]],
|
|
327
|
+
help: bool,
|
|
328
|
+
) -> Any:
|
|
329
|
+
validation_result, call_kwargs, resource = self._validate_action(
|
|
330
|
+
op_type=op_type,
|
|
331
|
+
action=action,
|
|
332
|
+
params=params,
|
|
333
|
+
help=help,
|
|
334
|
+
)
|
|
335
|
+
if validation_result is not None:
|
|
336
|
+
return validation_result
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
result = getattr(getattr(self.client, op_type), action)(**(call_kwargs or {}))
|
|
340
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
341
|
+
return self._handle_exception(exc, op_type=op_type, resource=resource, help=help)
|
|
342
|
+
return serialize(result)
|
|
343
|
+
|
|
344
|
+
async def _ainvoke(
|
|
345
|
+
self,
|
|
346
|
+
*,
|
|
347
|
+
op_type: str,
|
|
348
|
+
action: str,
|
|
349
|
+
params: Optional[Dict[str, Any]],
|
|
350
|
+
help: bool,
|
|
351
|
+
) -> Any:
|
|
352
|
+
validation_result, call_kwargs, resource = self._validate_action(
|
|
353
|
+
op_type=op_type,
|
|
354
|
+
action=action,
|
|
355
|
+
params=params,
|
|
356
|
+
help=help,
|
|
357
|
+
)
|
|
358
|
+
if validation_result is not None:
|
|
359
|
+
return validation_result
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
async_resource = getattr(self.async_client, op_type)
|
|
363
|
+
if not isinstance(async_resource, AsyncResource):
|
|
364
|
+
raise TypeError(f"client.{op_type} is not an async resource")
|
|
365
|
+
result = await getattr(async_resource, action)(**(call_kwargs or {}))
|
|
366
|
+
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
367
|
+
return self._handle_exception(exc, op_type=op_type, resource=resource, help=help)
|
|
368
|
+
return serialize(result)
|
gitcode_api/llm/mcp.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""FastMCP integration for the GitCode SDK LLM tool."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
|
4
|
+
|
|
5
|
+
from ._tool import TOOL_DESCRIPTION, TOOL_NAME, GitCodeLLMTool
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
from fastmcp.tools import Tool
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _missing_fastmcp_error() -> ImportError:
|
|
13
|
+
return ImportError("FastMCP support requires the optional dependency: pip install 'gitcode-api[mcp]'")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_fastmcp() -> tuple["type[FastMCP]", Callable[..., "Tool"]]:
|
|
17
|
+
try:
|
|
18
|
+
from fastmcp import FastMCP
|
|
19
|
+
from fastmcp.tools import tool as fastmcp_tool
|
|
20
|
+
except ImportError as exc:
|
|
21
|
+
raise _missing_fastmcp_error() from exc
|
|
22
|
+
return FastMCP, fastmcp_tool # type: ignore[return-value]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_mcp_gitcode_api_tool(tool: Optional[GitCodeLLMTool] = None) -> Any:
|
|
26
|
+
"""Return the async callable that can be registered with an MCP server.
|
|
27
|
+
|
|
28
|
+
:param tool: Optional preconfigured shared GitCode LLM tool.
|
|
29
|
+
:returns: Async callable using the standard GitCode tool parameters.
|
|
30
|
+
"""
|
|
31
|
+
wrapped = tool or GitCodeLLMTool()
|
|
32
|
+
|
|
33
|
+
async def gitcode_api_tool(
|
|
34
|
+
op_type: str, action: str = "", params: Optional[dict[str, Any]] = None, help: bool = False
|
|
35
|
+
) -> Any:
|
|
36
|
+
"""Call GitCode SDK resources using op_type, action, and params."""
|
|
37
|
+
return await wrapped.__async_call__(op_type=op_type, action=action, params=params, help=help)
|
|
38
|
+
|
|
39
|
+
gitcode_api_tool.__name__ = TOOL_NAME
|
|
40
|
+
gitcode_api_tool.__doc__ = TOOL_DESCRIPTION
|
|
41
|
+
return gitcode_api_tool
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def register_mcp_gitcode_api_tool(mcp: Union["FastMCP", Any], tool: Optional[GitCodeLLMTool] = None) -> Any:
|
|
45
|
+
"""Register the GitCode API tool with an existing FastMCP-compatible server.
|
|
46
|
+
|
|
47
|
+
:param mcp: FastMCP server instance.
|
|
48
|
+
:param tool: Optional preconfigured shared GitCode LLM tool.
|
|
49
|
+
:returns: The registered tool callable.
|
|
50
|
+
"""
|
|
51
|
+
callable_tool = create_mcp_gitcode_api_tool(tool)
|
|
52
|
+
if hasattr(mcp, "tool"):
|
|
53
|
+
try:
|
|
54
|
+
return mcp.tool(name=TOOL_NAME, description=TOOL_DESCRIPTION)(callable_tool)
|
|
55
|
+
except TypeError:
|
|
56
|
+
return mcp.tool()(callable_tool)
|
|
57
|
+
if hasattr(mcp, "add_tool"):
|
|
58
|
+
_, fastmcp_tool = _load_fastmcp()
|
|
59
|
+
mcp_tool = fastmcp_tool(callable_tool, name=TOOL_NAME, description=TOOL_DESCRIPTION)
|
|
60
|
+
return mcp.add_tool(mcp_tool)
|
|
61
|
+
raise TypeError("mcp must provide a tool decorator or add_tool method")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class GitCodeMCP:
|
|
65
|
+
"""Small FastMCP server wrapper exposing the GitCode API tool.
|
|
66
|
+
|
|
67
|
+
:param name: MCP server name.
|
|
68
|
+
:param tool: Optional preconfigured shared GitCode LLM tool.
|
|
69
|
+
:param kwargs: Forwarded to ``fastmcp.FastMCP``.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, name: str = "GitCode API", tool: Optional[GitCodeLLMTool] = None, **kwargs) -> None:
|
|
73
|
+
"""Create a FastMCP server and register the GitCode API tool."""
|
|
74
|
+
fastmcp, *_ = _load_fastmcp()
|
|
75
|
+
self.mcp = fastmcp(name, **kwargs)
|
|
76
|
+
self.gitcode_api_tool = register_mcp_gitcode_api_tool(self.mcp, tool)
|
|
77
|
+
|
|
78
|
+
def __getattr__(self, name: str) -> Any:
|
|
79
|
+
"""Delegate unknown attributes to the wrapped FastMCP server."""
|
|
80
|
+
return getattr(self.mcp, name)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def create_mcp_server(name: str = "GitCode API", tool: Optional[GitCodeLLMTool] = None, **kwargs) -> "FastMCP":
|
|
84
|
+
"""Create a FastMCP server with the GitCode API tool already registered."""
|
|
85
|
+
return GitCodeMCP(name=name, tool=tool, **kwargs).mcp
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
__all__ = [
|
|
89
|
+
"GitCodeMCP",
|
|
90
|
+
"create_mcp_gitcode_api_tool",
|
|
91
|
+
"create_mcp_server",
|
|
92
|
+
"register_mcp_gitcode_api_tool",
|
|
93
|
+
]
|