Coverage for repo_ctx / mcp_server.py: 0%
106 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-25 17:42 +0100
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-25 17:42 +0100
1"""MCP server implementation."""
2import json
3from mcp.server import Server
4from mcp.server.stdio import stdio_server
5from mcp.types import Tool, TextContent
6from .core import GitLabContext
7from .config import Config
10async def serve(
11 config_path: str = None,
12 gitlab_url: str = None,
13 gitlab_token: str = None,
14 github_url: str = None,
15 github_token: str = None,
16 storage_path: str = None
17):
18 """Run MCP server.
20 Args:
21 config_path: Optional path to config file
22 gitlab_url: Optional GitLab URL (overrides config file)
23 gitlab_token: Optional GitLab token (overrides config file)
24 github_url: Optional GitHub URL (overrides config file)
25 github_token: Optional GitHub token (overrides config file)
26 storage_path: Optional storage path (overrides config file)
27 """
28 # Load config with priority: CLI args > env vars > config files
29 try:
30 config = Config.load(
31 config_path=config_path,
32 gitlab_url=gitlab_url,
33 gitlab_token=gitlab_token,
34 github_url=github_url,
35 github_token=github_token,
36 storage_path=storage_path
37 )
38 except ValueError as e:
39 print(f"Configuration error: {e}")
40 raise
42 # Initialize core
43 context = GitLabContext(config)
44 await context.init()
46 # Create server
47 server = Server("repo-ctx")
49 @server.list_tools()
50 async def list_tools() -> list[Tool]:
51 return [
52 Tool(
53 name="gitlab-search-libraries",
54 description="Search for GitLab libraries/projects by name. Returns matching libraries with their IDs and available versions.",
55 inputSchema={
56 "type": "object",
57 "properties": {
58 "libraryName": {
59 "type": "string",
60 "description": "Library name to search for"
61 }
62 },
63 "required": ["libraryName"]
64 }
65 ),
66 Tool(
67 name="gitlab-fuzzy-search",
68 description="Fuzzy search for GitLab repositories. Returns top matches even with typos or partial names. Use this when you don't know the exact repository path.",
69 inputSchema={
70 "type": "object",
71 "properties": {
72 "query": {
73 "type": "string",
74 "description": "Search term (can be partial or fuzzy)"
75 },
76 "limit": {
77 "type": "integer",
78 "description": "Maximum results to return (default: 10)",
79 "default": 10
80 }
81 },
82 "required": ["query"]
83 }
84 ),
85 Tool(
86 name="gitlab-index-repository",
87 description="Index a repository from GitLab or GitHub to make its documentation searchable. Use format: group/project for GitLab, owner/repo for GitHub",
88 inputSchema={
89 "type": "object",
90 "properties": {
91 "repository": {
92 "type": "string",
93 "description": "Repository path (e.g., 'group/repo' or 'owner/repo')"
94 },
95 "provider": {
96 "type": "string",
97 "description": "Provider to use: 'gitlab', 'github', or 'auto' for auto-detection (default: auto)",
98 "enum": ["gitlab", "github", "auto"],
99 "default": "auto"
100 }
101 },
102 "required": ["repository"]
103 }
104 ),
105 Tool(
106 name="gitlab-index-group",
107 description="Index all repositories in a GitLab group or GitHub organization. Optionally include subgroups (GitLab only).",
108 inputSchema={
109 "type": "object",
110 "properties": {
111 "group": {
112 "type": "string",
113 "description": "Group/organization path (e.g., 'groupname' or 'orgname')"
114 },
115 "includeSubgroups": {
116 "type": "boolean",
117 "description": "Include subgroups - only works with GitLab (default: true)",
118 "default": True
119 },
120 "provider": {
121 "type": "string",
122 "description": "Provider to use: 'gitlab', 'github', or 'auto' (default: auto)",
123 "enum": ["gitlab", "github", "auto"],
124 "default": "auto"
125 }
126 },
127 "required": ["group"]
128 }
129 ),
130 Tool(
131 name="gitlab-get-docs",
132 description="Retrieve documentation for a specific GitLab library. Supports topic filtering and pagination.",
133 inputSchema={
134 "type": "object",
135 "properties": {
136 "libraryId": {
137 "type": "string",
138 "description": "Library ID in format /group/project or /group/project/version"
139 },
140 "topic": {
141 "type": "string",
142 "description": "Optional topic to filter documentation"
143 },
144 "page": {
145 "type": "integer",
146 "description": "Page number for pagination (default: 1)",
147 "default": 1
148 }
149 },
150 "required": ["libraryId"]
151 }
152 )
153 ]
155 @server.call_tool()
156 async def call_tool(name: str, arguments: dict) -> list[TextContent]:
157 if name == "gitlab-search-libraries":
158 library_name = arguments["libraryName"]
159 results = await context.search_libraries(library_name)
161 output = []
162 output.append("Available Libraries (search results):\n\n")
164 for result in results:
165 output.append(f"- Library ID: {result.library_id}\n")
166 output.append(f" Name: {result.name}\n")
167 output.append(f" Description: {result.description}\n")
168 output.append(f" Versions: {', '.join(result.versions)}\n")
169 output.append("\n")
171 if not results:
172 output.append(f"No libraries found matching '{library_name}'.\n")
173 output.append("Make sure the repository has been indexed first.\n")
175 return [TextContent(type="text", text="".join(output))]
177 elif name == "gitlab-fuzzy-search":
178 query = arguments["query"]
179 limit = arguments.get("limit", 10)
180 results = await context.fuzzy_search_libraries(query, limit)
182 output = []
183 output.append(f"Fuzzy search results for '{query}':\n\n")
185 for i, result in enumerate(results, 1):
186 output.append(f"{i}. {result.library_id}\n")
187 output.append(f" Name: {result.name}\n")
188 output.append(f" Group: {result.group}\n")
189 output.append(f" Description: {result.description}\n")
190 output.append(f" Match: {result.match_type} in {result.matched_field} (score: {result.score:.2f})\n")
191 output.append("\n")
193 if not results:
194 output.append(f"No repositories found matching '{query}'.\n")
195 output.append("Try a different search term or index repositories first using gitlab-index-repository or gitlab-index-group.\n")
196 else:
197 output.append(f"\nTo get documentation, use gitlab-get-docs with one of the Library IDs above.\n")
199 return [TextContent(type="text", text="".join(output))]
201 elif name == "gitlab-index-repository":
202 repository = arguments["repository"]
203 provider = arguments.get("provider", "auto")
204 parts = repository.split("/")
205 if len(parts) < 2:
206 return [TextContent(type="text", text=f"Error: Repository must be in format group/project or owner/repo")]
208 project = parts[-1]
209 group = "/".join(parts[:-1])
211 # Convert "auto" to None for auto-detection
212 provider_type = None if provider == "auto" else provider
214 try:
215 await context.index_repository(group, project, provider_type=provider_type)
216 provider_used = provider_type or "auto-detected"
217 return [TextContent(type="text", text=f"Successfully indexed {repository} using {provider_used} provider. You can now search for it using gitlab-fuzzy-search or gitlab-get-docs.")]
218 except Exception as e:
219 return [TextContent(type="text", text=f"Error indexing {repository}: {str(e)}")]
221 elif name == "gitlab-index-group":
222 group = arguments["group"]
223 include_subgroups = arguments.get("includeSubgroups", True)
224 provider = arguments.get("provider", "auto")
226 # Convert "auto" to None for auto-detection
227 provider_type = None if provider == "auto" else provider
229 try:
230 results = await context.index_group(group, include_subgroups, provider_type=provider_type)
231 output = []
232 provider_used = provider_type or "auto-detected"
233 output.append(f"Indexed group '{group}' using {provider_used} provider:\n\n")
234 output.append(f"Total projects: {results['total']}\n")
235 output.append(f"Successfully indexed: {len(results['indexed'])}\n")
236 output.append(f"Failed: {len(results['failed'])}\n\n")
238 if results['indexed']:
239 output.append("Indexed repositories:\n")
240 for repo in results['indexed'][:10]: # Show first 10
241 output.append(f" - {repo}\n")
242 if len(results['indexed']) > 10:
243 output.append(f" ... and {len(results['indexed']) - 10} more\n")
245 if results['failed']:
246 output.append("\nFailed repositories:\n")
247 for fail in results['failed'][:5]: # Show first 5 failures
248 output.append(f" - {fail['path']}: {fail['error']}\n")
250 return [TextContent(type="text", text="".join(output))]
251 except Exception as e:
252 return [TextContent(type="text", text=f"Error indexing group {group}: {str(e)}")]
254 elif name == "gitlab-get-docs":
255 library_id = arguments["libraryId"]
256 topic = arguments.get("topic")
257 page = arguments.get("page", 1)
259 try:
260 result = await context.get_documentation(library_id, topic, page)
261 return [TextContent(type="text", text=result["content"][0]["text"])]
262 except Exception as e:
263 return [TextContent(type="text", text=f"Error: {str(e)}")]
265 else:
266 return [TextContent(type="text", text=f"Unknown tool: {name}")]
268 # Run server with stdio transport
269 async with stdio_server() as (read_stream, write_stream):
270 await server.run(read_stream, write_stream, server.create_initialization_options())