The codeagent index engine currently indexes C# symbols (classes, methods, properties, etc.) and their relationships (calls, inherits, implements), but has no awareness of HTTP API boundaries. When an LLM agent navigates a codebase, knowing which methods are API endpoints — their routes, HTTP verbs, auth requirements, and request/response types — is critical for understanding the public surface area of a service.
This change adds detection and indexing of ASP.NET Core controller-based API endpoints. The architecture is designed to be extensible to Minimal APIs and gRPC services later.
Scope: ASP.NET Core controllers only (classes with [ApiController] or inheriting ControllerBase, action methods with [HttpGet]/[HttpPost]/etc.)
Metadata per endpoint: route template, HTTP method, authorization policy, request body type, response type, status codes, content types.
MCP surface: New search_api_endpoints tool + API metadata attached to existing get_symbol and get_file_outline responses.
File: crates/codeagent-core/src/db/schema.rs
A separate table (not columns on nodes) so that:
- Only API nodes get rows — no null-bloat for the 95% of symbols that aren't endpoints
- The 29-column
row_to_node()convention is preserved unchanged - ON DELETE CASCADE handles cleanup automatically
- Extensible for Minimal APIs and gRPC via the
api_stylecolumn
CREATE TABLE IF NOT EXISTS api_endpoints (
endpoint_id INTEGER PRIMARY KEY AUTOINCREMENT,
node_id BLOB(16) NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE,
controller_id BLOB(16) REFERENCES nodes(node_id) ON DELETE SET NULL,
api_style TEXT NOT NULL DEFAULT 'controller',
http_method TEXT,
route_template TEXT,
auth_policy TEXT,
request_body_type TEXT,
response_type TEXT,
status_codes TEXT,
consumes TEXT,
extractor_version TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_api_endpoints_node_id ON api_endpoints(node_id);
CREATE INDEX IF NOT EXISTS idx_api_endpoints_controller ON api_endpoints(controller_id);
CREATE INDEX IF NOT EXISTS idx_api_endpoints_http_method ON api_endpoints(http_method);
CREATE INDEX IF NOT EXISTS idx_api_endpoints_route ON api_endpoints(route_template);Update CURRENT_SCHEMA_VERSION from 3 → 4.
File: crates/codeagent-core/src/graph/api_endpoints.rs (NEW)
Structs:
ApiStyleenum:Controller,MinimalApi,Grpc(extensible, serde snake_case)ApiEndpointUpsert— write struct with all fieldsApiEndpoint— read-back struct (addsendpoint_id)
Functions:
upsert_api_endpoint(conn, &ApiEndpointUpsert) -> Result<()>delete_api_endpoints_for_file(conn, file_path) -> Result<usize>— deletes by joining onnodes.file_pathget_api_endpoint(conn, node_id) -> Result<Option<ApiEndpoint>>— single lookupget_api_endpoints_for_file(conn, file_path) -> Result<Vec<ApiEndpoint>>— batch lookup for outlinesearch_api_endpoints(conn, http_method?, route_pattern?, controller_id?, limit) -> Result<Vec<ApiEndpoint>>— filtered search (LIKE on route_template)
Re-export from graph/mod.rs.
File: crates/codeagent-core/src/adapters/csharp.rs
struct AspNetControllerContext {
is_api_controller: bool,
class_route_template: Option<String>,
class_auth_policy: Option<String>,
controller_node_id: Option<NodeId>,
controller_name: String,
}Add extract_aspnet_attributes(node, source) that walks attribute_list > attribute children on a declaration node and returns parsed data. Handles:
[ApiController]→ marks class as controller[Route("api/[controller]")]→ class-level route template[HttpGet],[HttpPost],[HttpPut],[HttpDelete],[HttpPatch]→ HTTP method + optional route fragment[Authorize],[Authorize(Policy = "...")]→ auth policy[AllowAnonymous]→auth_policy = "anonymous"[ProducesResponseType(typeof(T), statusCode)]→ response type + status codes[Consumes("...")]→ request content type
resolve_route_template(class_route, method_route, controller_name):
- Replace
[controller]token with controller name minus "Controller" suffix, lowercased - Combine class + method route segments with
/
- Add
controller_ctx: Option<AspNetControllerContext>field toParseContext - In
handle_class: extract attributes; if[ApiController]is present (or class inherits fromControllerBase), populatecontroller_ctxbefore walking children. Clear it after. - In
handle_method: ifcontroller_ctx.is_some(), extract method-level attributes, resolve the full route, detect[FromBody]parameter type, extract response type fromActionResult<T>/Task<ActionResult<T>>, then calldelete + upsert_api_endpoint()withextractor_version = "treesitter-cs". - Also detect
ControllerBaseinheritance inhandle_class'semit_base_type_edges— check if any base type name contains "ControllerBase" or "Controller" as a heuristic.
File: crates/codeagent-core/src/ipc/protocol.rs
Add:
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiEndpointInfo {
pub http_method: Option<String>,
pub route_template: Option<String>,
pub auth_policy: Option<String>,
pub request_body_type: Option<String>,
pub response_type: Option<String>,
pub status_codes: Option<String>,
pub consumes: Option<String>,
}Add optional field to SemanticNode:
#[serde(skip_serializing_if = "Option::is_none")]
pub api_endpoint: Option<ApiEndpointInfo>,Backward-compatible: older extractors that omit this field deserialize as None.
File: extractors/csharp/src/Protocol.cs
- Add
ApiEndpointInfoclass with matching JSON properties - Add
ApiEndpointproperty toSemanticNode
File: extractors/csharp/src/RoslynExtractor.cs
- Add
ExtractApiEndpoint(IMethodSymbol, SemanticModel)that usessymbol.GetAttributes()to authoritatively extract:- HTTP method attributes (
HttpGetAttribute, etc.) - Route resolution from
RouteAttributeon both class and method AuthorizeAttribute/AllowAnonymousAttributewith policy namesProducesResponseTypeAttributefor response types and status codesConsumesAttributefor content typesFromBodyAttributeon parameters for request body type
- HTTP method attributes (
- Attach
ApiEndpointInfoto the correspondingSemanticNodefor each controller action
File: crates/codeagent-core/src/ingest/semantic.rs
In enrich_file(), after identity reconciliation, add a step that processes SemanticNode.api_endpoint:
- Look up the method's
node_idby location (existing pattern) - Find the controller
node_idvia theContainsedge (parent class) - Delete existing
api_endpointsrows for thisnode_id - Insert authoritative Roslyn-derived endpoint data with
extractor_version = "roslyn"
This overwrites the tree-sitter approximation when Roslyn is available.
File: crates/codeagent-mcp/src/tools/search.rs
Params:
struct SearchApiEndpointsParams {
http_method: Option<String>, // "GET", "POST", etc.
route_pattern: Option<String>, // Substring match on route_template
controller_id: Option<String>, // UUID filter
limit: Option<usize>, // default 20, max 50
}Response: array of objects, each with endpoint (API metadata) + method (node metadata: name, qualified_name, file_path, line_start, parameter_signature, return_type).
File: crates/codeagent-mcp/src/state.rs
- Register the new tool with
#[tool(description = "Search API endpoints by HTTP method, route, or controller")] - Total tool count: 18 → 19
File: crates/codeagent-mcp/src/serialization.rs
- Add
api_endpoint_to_json(ep, node)function
File: crates/codeagent-mcp/src/tools/navigation.rs
After fetching the node, also load get_api_endpoint(conn, node_id). If present, attach an api_endpoint key to the JSON response.
After fetching outline nodes, batch-load get_api_endpoints_for_file(conn, file_path) into a HashMap. When serializing each outline node, attach api_endpoint (http_method + route_template) if present.
db/schema.rs— addMIGRATION_004, bumpCURRENT_SCHEMA_VERSIONto 4graph/api_endpoints.rs— new file with structs + all CRUD functionsgraph/mod.rs— addpub mod api_endpointsand re-exports- Write unit tests for CRUD operations + ON DELETE CASCADE
cargo test -p codeagent-core— verify 0 regressions
adapters/csharp.rs— add attribute extraction helpers +AspNetControllerContextadapters/csharp.rs— wire intohandle_classandhandle_method- Write adapter tests with sample ASP.NET Core controller source
cargo test -p codeagent-core— verify all tests pass
ipc/protocol.rs— addApiEndpointInfostruct + field onSemanticNodeextractors/csharp/src/Protocol.cs— addApiEndpointInfoclassextractors/csharp/src/RoslynExtractor.cs— addExtractApiEndpointlogicingest/semantic.rs— processapi_endpointfrom semantic nodes- Write IPC serde backward-compatibility tests
cargo test -p codeagent-core
serialization.rs— addapi_endpoint_to_jsontools/search.rs— addsearch_api_endpointshandlertools/navigation.rs— extendget_symbolandget_file_outlinestate.rs— register new tool- Write MCP integration tests
cargo test -p codeagent-mcp
- Update
MEMORY.mdwith new conventions - Update
TESTS_IMPLEMENTATION_PLAN.mdwith new test entries - Run
Sync-TestCoverage.ps1 - Full
cargo testto confirm
- Migration 004 creates table successfully
upsert_api_endpoint+get_api_endpointroundtripsearch_api_endpointsfilters by http_method, route_pattern, controller_iddelete_api_endpoints_for_fileremoves correct rows- ON DELETE CASCADE removes endpoints when method node is hard-deleted
- Basic
[ApiController]+[HttpGet]detection - Route resolution:
[Route("api/[controller]")]+[HttpGet("{id}")]→api/users/{id} - Auth extraction:
[Authorize(Policy = "Admin")],[AllowAnonymous] [FromBody]parameter type extraction- Response type from
ActionResult<T>andTask<ActionResult<T>> - Non-controller classes produce no
api_endpointsrows - Class without
[ApiController]attribute is ignored
SemanticNodewithapi_endpointdeserializes correctly- Backward compat: missing
api_endpointfield deserializes asNone
search_api_endpointsreturns filtered resultsget_symbolincludesapi_endpointfor action methodsget_symbolomitsapi_endpointfor non-endpoint nodesget_file_outlineincludesapi_endpointfor action methods
-
Tree-sitter attribute accuracy: Tree-sitter extraction is best-effort (textual matching). Roslyn provides authoritative data that overwrites tree-sitter on semantic enrichment. Both paths produce valid data; Roslyn is just more precise.
-
29-column
row_to_node()convention: Preserved — no changes tonodestable. API data is in a separate table with its own read functions. -
Deletion safety:
ON DELETE CASCADEonapi_endpoints.node_id— no manual cleanup needed when nodes are deleted. -
IPC backward compat: The
api_endpointfield onSemanticNodeisOptionwithskip_serializing_if. Older extractors that don't produce it deserialize asNone. -
Schema migration: Additive only (new table, no ALTER TABLE). Existing databases upgrade cleanly.