File size: 16,206 Bytes
9d6b77b
3516027
 
 
 
 
19c605a
acc20c1
5b53be2
3516027
 
e948c11
3516027
329c6dd
 
 
19c605a
acc20c1
7f82b3d
acc20c1
 
7f82b3d
3516027
 
 
19c605a
3516027
 
19c605a
3516027
 
 
19c605a
3516027
 
 
 
 
 
 
 
 
 
 
e948c11
3516027
f5a007a
8557742
e948c11
 
 
 
f5a007a
e948c11
 
 
f5a007a
e948c11
f5a007a
3516027
 
e948c11
3516027
 
 
e948c11
7f82b3d
ef321a7
e8f51cf
 
329c6dd
13489f0
b823bbe
13489f0
 
329c6dd
 
 
 
 
 
 
 
 
 
 
 
e8f51cf
3516027
19c605a
 
 
7f82b3d
 
 
19c605a
3516027
 
9f06dc4
 
 
 
 
19c605a
e8f51cf
51d0f96
3516027
 
19c605a
5b53be2
5370a22
19c605a
 
 
533df7e
19c605a
 
 
533df7e
 
e948c11
533df7e
3516027
329c6dd
533df7e
3516027
 
525e485
 
 
 
8557742
525e485
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef321a7
 
525e485
 
329c6dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525e485
 
19c605a
 
 
7f82b3d
 
 
19c605a
525e485
 
 
 
 
 
 
19c605a
 
525e485
 
 
 
19c605a
5b53be2
5370a22
19c605a
 
525e485
 
 
 
 
 
 
 
329c6dd
525e485
 
3516027
363d5b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ef321a7
 
363d5b1
 
329c6dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363d5b1
 
19c605a
 
 
7f82b3d
 
 
8557742
363d5b1
 
 
 
 
 
 
19c605a
363d5b1
 
 
 
19c605a
5b53be2
5370a22
19c605a
 
363d5b1
 
 
 
 
 
 
 
329c6dd
363d5b1
 
3516027
8557742
e4fc996
7693ee9
 
 
 
 
 
 
 
 
 
 
 
19c605a
8557742
e4fc996
7693ee9
 
 
 
fd927e8
7693ee9
 
8557742
7693ee9
 
 
19c605a
8557742
e4fc996
7693ee9
8557742
7693ee9
 
 
 
 
 
 
 
 
 
 
19c605a
8557742
e4fc996
7693ee9
8557742
7693ee9
8557742
 
 
 
 
 
 
 
7693ee9
8557742
7693ee9
 
19c605a
8557742
e4fc996
8557742
7693ee9
9d6b77b
7693ee9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8557742
 
 
 
 
 
 
e4fc996
8557742
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7693ee9
3516027
19c605a
ddbbdce
 
4c2b48a
 
ddbbdce
8557742
 
 
4c2b48a
 
8557742
3516027
876fb90
acc20c1
d255229
acc20c1
329c6dd
5b53be2
24961f7
7f82b3d
5b53be2
acc20c1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
from typing import AsyncIterator
from contextlib import asynccontextmanager
import httpx
from fastmcp import FastMCP, Context
from loguru import logger
from dataclasses import dataclass
import uuid
import os
import sys

from utils.config import API_CONFIG
from utils.model import MultiModuleRequest, SingleModuleRequest, SearchSpaceRoutingRequest

from fastmcp.server.dependencies import get_http_request
from starlette.requests import Request


from dotenv import load_dotenv

load_dotenv()


@dataclass
class DSContext:
    """Typed context for DS MCP server"""

    client: httpx.AsyncClient


@asynccontextmanager
async def ds_lifespan(server: FastMCP) -> AsyncIterator[DSContext]:
    client = httpx.AsyncClient(timeout=API_CONFIG["timeout"])
    ctx = DSContext(client=client)

    try:
        yield ctx
    finally:
        await client.aclose()


mcp = FastMCP(name="defect-solver-server", lifespan=ds_lifespan)


@mcp.tool(
    name="multi_module_bug_localization",
    description=(
        """
        Performs bug localization across the entire microservice architecture by first identifying likely microservices
        (search spaces) and then locating the bug-related files within them.

        Use this tool when you do not know which microservice is responsible for the bug.

        Args:
            request (MultiModuleRequest): Issue key and descriptive details from the bug report.
            ctx (Context): MCP context object.

        Returns:
            dict: Includes selected microservices and localized file paths.
        """
    ),
)
async def multi_module_bug_localization(request: MultiModuleRequest, ctx: Context) -> dict:
    ds_ctx: DSContext = ctx.request_context.lifespan_context
    client = ds_ctx.client

    api_url = API_CONFIG["api_base_url"] + API_CONFIG["api_multimodule_endpoint"]

    # hf_token to access private HF Space serving Defect Solver API
    hf_token = API_CONFIG.get("hf_access_token", None)

    # Get per-user API key from MCP Context
    user_request: Request = get_http_request()
    logger.info("Received request headers: {}".format(user_request.headers))
    defect_solver_api_key = user_request.headers.get("DS-API-Key", None)
    logger.info("defect_solver_api_key: {}".format(defect_solver_api_key) )
    logger.info(
        "Using API key: {}".format(defect_solver_api_key)
        if defect_solver_api_key
        else "No Defect Solver API key provided"
    )

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {hf_token}" if hf_token else None,
        "DS-API-Key": defect_solver_api_key,
    }
    headers = {k: v for k, v in headers.items() if v is not None}

    try:
        # Generate a UUID for issue_key even if provided (overwrite)
        request.issue_key = str(uuid.uuid4())

        logger.info(
            f"Sending multi-module bug localization request to {api_url} with data: {str(request.model_dump())[:300]}..."
        )

        response = await client.post(
            api_url,
            json={
                "key": request.issue_key,
                "fields": {
                    "summary": request.summary,
                    "description": request.description,
                },
            },
            headers=headers,
        )
        response.raise_for_status()

        logger.info(f"Received response: {str(response.text)[:300]}...")
        logger.debug(f"Response JSON: {response.json()}")

        return {"issue_key": request.issue_key, "result": response.json()}

    except httpx.HTTPStatusError as e:
        logger.error(
            f"HTTP error {e.response.status_code} from multimodule bug localization endpoint: {e.response.text}"
        )
        return {"error": f"HTTP error: {e.response.status_code}", "details": e.response.text}
    except httpx.RequestError as e:
        logger.error(f"Request error while calling multimodule bug localization endpoint: {e}")
        return {"error": "Request error", "details": str(e)}
    except Exception as e:
        logger.error("Unexpected error during multimodule bug localization request")
        return {"error": "Unexpected error", "details": str(e)}


@mcp.tool(
    name="single_module_bug_localization",
    description=(
        """
        Performs bug localization within a specific module (microservice) of a microservice architecture.

        Use this tool when you already know the module where the bug is likely located.

        Args:
            request (SingleModuleRequest): Bug description, issue key, and the known module name.
            ctx (Context): The MCP context containing the lifespan context.

        Returns:
            dict: Includes the selected module and localized file paths.
        """
    ),
)
async def single_module_bug_localization(request: SingleModuleRequest, ctx: Context) -> dict:
    ds_ctx: DSContext = ctx.request_context.lifespan_context
    client = ds_ctx.client

    api_url = API_CONFIG["api_base_url"] + API_CONFIG["api_singlemodule_endpoint"]

    # hf token to access Defect Solver API
    hf_token = API_CONFIG.get("hf_access_token", None)

    # Get per-user API key from MCP Context
    user_request: Request = get_http_request()
    defect_solver_api_key = user_request.headers.get("DS-API-Key", None)
    logger.info(
        "Using API key: {}".format(defect_solver_api_key)
        if defect_solver_api_key
        else "No Defect Solver API key provided"
    )

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {hf_token}" if hf_token else None,
        "DS-API-Key": defect_solver_api_key,
    }
    headers = {k: v for k, v in headers.items() if v is not None}

    try:
        # Generate a UUID for issue_key even if provided (overwrite)
        request.issue_key = str(uuid.uuid4())

        logger.info(
            f"Sending single-module bug localization request to {api_url} with data: {str(request.model_dump())[:300]}..."
        )

        response = await client.post(
            api_url,
            json={
                "key": request.issue_key,
                "fields": {
                    "summary": request.summary,
                    "description": request.description,
                    "module": request.module,
                },
            },
            headers=headers,
        )
        response.raise_for_status()

        logger.info(f"Received response: {str(response.text)[:300]}...")
        logger.debug(f"Response JSON: {response.json()}")

        return {"issue_key": request.issue_key, "result": response.json()}

    except httpx.HTTPStatusError as e:
        logger.error(f"HTTP error {e.response.status_code} from endpoint: {e.response.text}")
        return {"error": f"HTTP error: {e.response.status_code}", "details": e.response.text}
    except httpx.RequestError as e:
        logger.error(f"Request error while calling endpoint: {e}")
        return {"error": "Request error", "details": str(e)}
    except Exception as e:
        logger.error("Unexpected error during single-module bug localization")
        return {"error": "Unexpected error", "details": str(e)}


@mcp.tool(
    name="search_space_routing",
    description=(
        """
        Identifies and returns the most likely microservices (search spaces) that could be the source of a reported bug.

        Use this tool when the source of the bug is unknown and you want to narrow down the investigation to top candidate microservices.

        Args:
            request (SearchSpaceRoutingRequest): Bug description and optional issue key/summary.
            ctx (Context): The MCP context with lifespan context.

        Returns:
            dict: A message and the list of selected search spaces (microservices).
        """
    ),
)
async def search_space_routing(request: SearchSpaceRoutingRequest, ctx: Context) -> dict:
    ds_ctx: DSContext = ctx.request_context.lifespan_context
    client = ds_ctx.client

    api_url = API_CONFIG["api_base_url"] + API_CONFIG["api_searchspace_endpoint"]

    # hf_token to access private HF Space serving Defect Solver API
    hf_token = API_CONFIG.get("hf_access_token", None)

    # Get per-user API key from MCP Context
    user_request: Request = get_http_request()
    defect_solver_api_key = user_request.headers.get("DS-API-Key", None)
    logger.info(
        "Using API key: {}".format(defect_solver_api_key)
        if defect_solver_api_key
        else "No Defect Solver API key provided"
    )

    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {hf_token}" if hf_token else None,
        "DS-API-Key": defect_solver_api_key,
    }
    headers = {k: v for k, v in headers.items() if v is not None}

    try:
        # Generate a UUID for issue_key even if provided (overwrite)
        request.issue_key = str(uuid.uuid4())

        logger.info(
            f"Sending search space routing request to {api_url} with data: {str(request.model_dump())[:300]}..."
        )

        response = await client.post(
            api_url,
            json={
                "key": request.issue_key,
                "fields": {
                    "summary": request.summary,
                    "description": request.description,
                },
            },
            headers=headers,
        )
        response.raise_for_status()

        logger.info(f"Received response: {str(response.text)[:300]}...")
        logger.debug(f"Response JSON: {response.json()}")

        return {"issue_key": request.issue_key, "result": response.json()}

    except httpx.HTTPStatusError as e:
        logger.error(f"HTTP error {e.response.status_code} from endpoint: {e.response.text}")
        return {"error": f"HTTP error: {e.response.status_code}", "details": e.response.text}
    except httpx.RequestError as e:
        logger.error(f"Request error while calling endpoint: {e}")
        return {"error": "Request error", "details": str(e)}
    except Exception as e:
        logger.error("Unexpected error during search space routing")
        return {"error": "Unexpected error", "details": str(e)}


@mcp.prompt(title="Select Bug Localization Tool")
async def prompt_select_tool() -> str:
    return """
You have access to bug localization tools in this MCP server. Determine the best approach for the current bug based on the description provided in this conversation.

Recommend:
1. **Primary Tool** - Which tool to use first and why
2. **Tool Sequence** - If multiple tools needed, in what order
3. **Reasoning** - Why this approach is optimal
4. **Expected Workflow** - Step-by-step tool usage plan

Provide specific tool recommendations with rationale.
"""


@mcp.prompt(title="Localize and Find Bug Using Selected Tool")
async def prompt_find_bug() -> str:
    return """
You are using a bug localization tool to identify potential files related to the bug described in this conversation. Execute the following steps:

1. **Input the augmented bug description** - Use the detailed description from the previous step
2. **Run the localization tool** - Execute the tool to find areas related to the bug
3. **Collect results** - Gather the results identified by the tool
4. **Return results** - Provide the results and any additional metadata

The tool will return info that are likely related to the bug.
"""


@mcp.prompt(title="Explain Localization Results")
async def prompt_explain() -> str:
    return """
You received a response from a bug localization tool in this conversation. Interpret and explain the results for actionable next steps.

Analyze and provide:
1. **Key Files Identified** - Prioritized list of files to investigate
2. **Investigation Order** - Which files to check first and why  
3. **Code Patterns to Look For** - Specific methods, classes, or patterns
4. **Next Tool Recommendations** - Should you use single_module on specific modules?
5. **Confidence Assessment** - How reliable are these results?

Provide actionable guidance for the developer's next steps.
"""


@mcp.prompt(title="Fix Localized Bug")
async def prompt_fix_bug() -> str:
    return """
You have codebase access. A bug localization tool has identified files as potential starting points in this conversation.

Instructions:
1. **Directly fix the bug** - Use the identified files as entry points, but your goal is to discover the root cause and apply the necessary code changes.
2. **Explore the codebase** - Investigate related code, dependencies, and connections as needed.
3. **Make concrete changes** - Show the exact code modifications required to resolve the bug, with before/after code snippets.
4. **Do not just analyze** - You must provide and apply the fix, not just discuss possible causes.
5. **Suggest verification** - Briefly describe how to test and confirm the fix works.

Your task is to discover, modify, and fix the bug in the codebase using the provided starting points.

NOTE: If you cannot find the bug and fix it, provide a brief explanation of your investigation process and why you could not resolve it.
"""


@mcp.prompt(title="Full Bug Resolution Workflow")
async def prompt_full_workflow() -> str:
    return """
You are a bug resolution expert with access to this codebase and bug localization tools. Complete the full workflow from bug description to fix.

Follow this workflow:

1. **AUGMENT** - First, examine the codebase and augment the bug description with technical details, specific modules, file types, and architectural context.

2. **ROUTE** - Decide which bug localization tool to use:
   - search_space_routing: If you don't know which microservices are involved
   - multi_module_bug_localization: If multiple modules likely affected
   - single_module_bug_localization: If you know the specific module

3. **LOCALIZE** - Call the appropriate tool(s) with the augmented description.

4. **INTERPRET** - Analyze the localization results to identify key files and investigation priorities.

5. **FIX** - Examine the identified files (as starting points), find the actual bug, and provide concrete code fixes.

Execute each step and provide the final resolution with specific code changes.

===== Bug Description =====
<insert_here>
"""


@mcp.prompt(title="Augment Bug report with Technical Details")
async def prompt_augment_bug_report() -> str:
    return """
You are a bug localization expert with access to this codebase. Your task is to augment the bug description from this conversation with technical details that help pinpoint the bug location.

Steps:
1. **Analyze the codebase** - Examine project structure, modules, and architectural patterns
2. **Map bug to code** - Identify likely affected areas based on the bug symptoms
3. **Augment the description** - Add:
   - Specific module/package/class names that might be involved
   - File types, naming patterns, and directory paths
   - Technical keywords, error types, and exception names
   - Affected architectural layers and components
   - Cross-cutting concerns and dependencies
   - External system interactions or API calls
   - Component relationships that could cause the bug

Return a 2-3x more detailed bug report with precise technical terms for effective bug localization, while preserving the original description.

===== Bug Description =====
<insert_here>
"""


@mcp.prompt(title="Revise Bug Report")
async def prompt_revise_bug_report() -> str:
    return """
Enhance the bug description from this conversation by adding relevant technical context that supports precise root cause analysis.
Return a 2-3x more detailed/enhanced bug report with precise technical terms for effective bug localization, while preserving the original description.

===== Bug Description =====
<insert_here>
"""


if __name__ == "__main__":
    TRANSPORT_MODE = os.getenv("TRANSPORT_MODE", "http")
    HOST = os.getenv("HOST", "0.0.0.0")
    PORT = int(os.getenv("PORT", "7860"))
    LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")

    # Configure logger
    logger.remove()
    logger.add(sys.stderr, level=LOG_LEVEL, format="{level}:\t\t{time:YYYY-MM-DD HH:mm:ss} - {message}")

    mcp.run(transport=TRANSPORT_MODE, host=HOST, port=PORT, log_level=LOG_LEVEL)