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.
@@ -1,7 +1,7 @@
1
1
  """ASCII art banner for command-line interface."""
2
2
 
3
- from functools import lru_cache
4
3
  import sys
4
+ from functools import lru_cache
5
5
 
6
6
  # ANSI Colour Codes
7
7
  CREDBG = "\033[41m"
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).removeprefix("Synchronous ").capitalize() or ""
77
- for line in doc.splitlines():
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="resources",
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).removeprefix("Synchronous ").capitalize() or ""
230
- class_lead = class_doc.split("\n\n", maxsplit=1)[0].strip() if class_doc else ""
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).removeprefix("Synchronous ").capitalize() or ""
258
- summary = _method_cli_summary(doc)
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
+ ]
@@ -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
+ ]