| 8477 | } |
| 8478 | |
| 8479 | static char *build_snippet_response(cbm_mcp_server_t *srv, cbm_node_t *node, |
| 8480 | const char *match_method, bool include_neighbors, |
| 8481 | cbm_node_t *alternatives, int alt_count) { |
| 8482 | char *root_path = get_project_root(srv, node->project); |
| 8483 | |
| 8484 | int start = node->start_line > 0 ? node->start_line : SKIP_ONE; |
| 8485 | int end = node->end_line > start ? node->end_line : start + SNIPPET_DEFAULT_LINES; |
| 8486 | /* Context-bomb guard: a structural node (Module/File) spans its whole file, |
| 8487 | * so an unclipped read returned the ENTIRE source — a field-eval agent that |
| 8488 | * fell back to a Module snippet pulled 400KB in one call. Cap the line span |
| 8489 | * (far above any real function) and flag it; the exact range is still in |
| 8490 | * start_line/end_line for a targeted re-read. */ |
| 8491 | bool snippet_clipped = false; |
| 8492 | if (end - start + 1 > MCP_SNIPPET_MAX_LINES) { |
| 8493 | end = start + MCP_SNIPPET_MAX_LINES - 1; |
| 8494 | snippet_clipped = true; |
| 8495 | } |
| 8496 | char *abs_path = NULL; |
| 8497 | char *source = resolve_snippet_source(root_path, node->file_path, start, end, &abs_path); |
| 8498 | |
| 8499 | yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); |
| 8500 | yyjson_mut_val *root_obj = yyjson_mut_obj(doc); |
| 8501 | yyjson_mut_doc_set_root(doc, root_obj); |
| 8502 | |
| 8503 | yyjson_mut_obj_add_str(doc, root_obj, "name", node->name ? node->name : ""); |
| 8504 | yyjson_mut_obj_add_str(doc, root_obj, "qualified_name", |
| 8505 | node->qualified_name ? node->qualified_name : ""); |
| 8506 | yyjson_mut_obj_add_str(doc, root_obj, "label", node->label ? node->label : ""); |
| 8507 | |
| 8508 | const char *display_path = ""; |
| 8509 | if (abs_path) { |
| 8510 | display_path = abs_path; |
| 8511 | } else if (node->file_path) { |
| 8512 | display_path = node->file_path; |
| 8513 | } |
| 8514 | yyjson_mut_obj_add_str(doc, root_obj, "file_path", display_path); |
| 8515 | yyjson_mut_obj_add_int(doc, root_obj, "start_line", start); |
| 8516 | yyjson_mut_obj_add_int(doc, root_obj, "end_line", end); |
| 8517 | if (snippet_clipped) { |
| 8518 | yyjson_mut_obj_add_bool(doc, root_obj, "source_clipped", true); |
| 8519 | yyjson_mut_obj_add_int(doc, root_obj, "clipped_at_lines", MCP_SNIPPET_MAX_LINES); |
| 8520 | } |
| 8521 | |
| 8522 | if (source) { |
| 8523 | char *safe_source = sanitize_utf8_lossy(source); |
| 8524 | if (safe_source) { |
| 8525 | yyjson_mut_obj_add_strcpy(doc, root_obj, "source", safe_source); |
| 8526 | free(safe_source); |
| 8527 | } else { |
| 8528 | yyjson_mut_obj_add_str(doc, root_obj, "source", "(source not available)"); |
| 8529 | } |
| 8530 | } else { |
| 8531 | yyjson_mut_obj_add_str(doc, root_obj, "source", "(source not available)"); |
| 8532 | } |
| 8533 | |
| 8534 | /* match_method — omitted for exact matches */ |
| 8535 | if (match_method) { |
| 8536 | yyjson_mut_obj_add_str(doc, root_obj, "match_method", match_method); |
no test coverage detected