VCL — 18 Operations for AI Agents
VCL runs at the cache edge — Varnish and Fastly logic deciding what gets served, stored, or passed. act101 surfaces subroutines and state transitions structurally, so agents reason about request flow at the CDN layer.
This page is the canonical reference an AI coding agent uses to refactor, query, and analyze VCL code through the act MCP server. 18 operations available: 0 refactor, 18 query, 0 analysis. Each operation is callable from Claude Code, Cursor, Codex, OpenCode, or any MCP-compatible agent host. Click any operation for a stable anchor link suitable for citation.
Worked VCL examples
act101 reads a VCL file's vcl version header, import, acl, and backend declarations as block declarations, its import statement as an import declaration, and each sub subroutine as a function declaration — the only callable unit this grammar has. symbols covers the identical set, but the version header is absent entirely and the acl/backend declarations come back kind: unknown rather than block; import and the two subroutines keep their kinds in both queries. The unit of structure in this grammar is the top-level declaration: statements inside a subroutine's body, such as if, set, and return, are not read as declarations by either query. Each example below is the verbatim output of the command shown, run against the file shown. Query outputs are pretty-printed with the timing block omitted.
Read the declarations and subroutines as a skeleton
arrivals.vcl caches a transit real-time-arrivals API in front of a transit_api backend: a purge_allowed ACL gates cache purges, vcl_recv decides how to handle each request, and vcl_backend_response sets a short TTL on arrivals responses.
$ act query skeleton arrivals.vcl
Before
vcl 4.1;
import directors;
backend transit_api {
.host = "10.0.6.20";
.port = "8080";
}
acl purge_allowed {
"127.0.0.1";
"10.0.6.0"/24;
}
sub vcl_recv {
if (req.method == "PURGE") {
if (client.ip !~ purge_allowed) {
return (synth(403, "Forbidden"));
}
return (purge);
}
if (req.url ~ "^/arrivals/") {
unset req.http.Cookie;
}
return (hash);
}
sub vcl_backend_response {
if (bereq.url ~ "^/arrivals/") {
set beresp.ttl = 15s;
}
return (deliver);
}
Output
{
"type": "Skeleton",
"declarations": [
{
"kind": "block",
"name": "4.1",
"range": {
"start": {
"file": "arrivals.vcl",
"line": 1,
"column": 1,
"byte_offset": 0
},
"end": {
"file": "arrivals.vcl",
"line": 1,
"column": 9,
"byte_offset": 8
}
},
"name_range": {
"start": {
"file": "arrivals.vcl",
"line": 1,
"column": 5,
"byte_offset": 4
},
"end": {
"file": "arrivals.vcl",
"line": 1,
"column": 8,
"byte_offset": 7
}
}
},
{
"kind": "import",
"name": "directors",
"range": {
"start": {
"file": "arrivals.vcl",
"line": 3,
"column": 1,
"byte_offset": 10
},
"end": {
"file": "arrivals.vcl",
"line": 3,
"column": 18,
"byte_offset": 27
}
},
"name_range": {
"start": {
"file": "arrivals.vcl",
"line": 3,
"column": 8,
"byte_offset": 17
},
"end": {
"file": "arrivals.vcl",
"line": 3,
"column": 17,
"byte_offset": 26
}
}
},
{
"kind": "block",
"name": "transit_api",
"range": {
"start": {
"file": "arrivals.vcl",
"line": 5,
"column": 1,
"byte_offset": 29
},
"end": {
"file": "arrivals.vcl",
"line": 8,
"column": 2,
"byte_offset": 97
}
},
"name_range": {
"start": {
"file": "arrivals.vcl",
"line": 5,
"column": 9,
"byte_offset": 37
},
"end": {
"file": "arrivals.vcl",
"line": 5,
"column": 20,
"byte_offset": 48
}
}
},
{
"kind": "block",
"name": "purge_allowed",
"range": {
"start": {
"file": "arrivals.vcl",
"line": 10,
"column": 1,
"byte_offset": 99
},
"end": {
"file": "arrivals.vcl",
"line": 13,
"column": 2,
"byte_offset": 156
}
},
"name_range": {
"start": {
"file": "arrivals.vcl",
"line": 10,
"column": 5,
"byte_offset": 103
},
"end": {
"file": "arrivals.vcl",
"line": 10,
"column": 18,
"byte_offset": 116
}
}
},
{
"kind": "function",
"name": "vcl_recv",
"range": {
"start": {
"file": "arrivals.vcl",
"line": 15,
"column": 1,
"byte_offset": 158
},
"end": {
"file": "arrivals.vcl",
"line": 26,
"column": 2,
"byte_offset": 426
}
},
"name_range": {
"start": {
"file": "arrivals.vcl",
"line": 15,
"column": 5,
"byte_offset": 162
},
"end": {
"file": "arrivals.vcl",
"line": 15,
"column": 13,
"byte_offset": 170
}
}
},
{
"kind": "function",
"name": "vcl_backend_response",
"range": {
"start": {
"file": "arrivals.vcl",
"line": 28,
"column": 1,
"byte_offset": 428
},
"end": {
"file": "arrivals.vcl",
"line": 33,
"column": 2,
"byte_offset": 551
}
},
"name_range": {
"start": {
"file": "arrivals.vcl",
"line": 28,
"column": 5,
"byte_offset": 432
},
"end": {
"file": "arrivals.vcl",
"line": 28,
"column": 25,
"byte_offset": 452
}
}
}
]
}
The skeleton reports the 4.1 version header, transit_api, and purge_allowed as block declarations, directors as an import, and vcl_recv/vcl_backend_response as function declarations.
List the same declarations as symbols
purge_allowed lists two client networks that are allowed to send PURGE requests.
$ act query symbols arrivals.vcl
Before
vcl 4.1;
import directors;
backend transit_api {
.host = "10.0.6.20";
.port = "8080";
}
acl purge_allowed {
"127.0.0.1";
"10.0.6.0"/24;
}
sub vcl_recv {
if (req.method == "PURGE") {
if (client.ip !~ purge_allowed) {
return (synth(403, "Forbidden"));
}
return (purge);
}
if (req.url ~ "^/arrivals/") {
unset req.http.Cookie;
}
return (hash);
}
sub vcl_backend_response {
if (bereq.url ~ "^/arrivals/") {
set beresp.ttl = 15s;
}
return (deliver);
}
Output
{
"type": "Symbols",
"symbols": [
{
"name": "directors",
"kind": "import",
"range": {
"start": {
"file": "arrivals.vcl",
"line": 3,
"column": 8,
"byte_offset": 17
},
"end": {
"file": "arrivals.vcl",
"line": 3,
"column": 17,
"byte_offset": 26
}
},
"visibility": "unknown"
},
{
"name": "transit_api",
"kind": "unknown",
"range": {
"start": {
"file": "arrivals.vcl",
"line": 5,
"column": 9,
"byte_offset": 37
},
"end": {
"file": "arrivals.vcl",
"line": 5,
"column": 20,
"byte_offset": 48
}
},
"visibility": "unknown"
},
{
"name": "purge_allowed",
"kind": "unknown",
"range": {
"start": {
"file": "arrivals.vcl",
"line": 10,
"column": 5,
"byte_offset": 103
},
"end": {
"file": "arrivals.vcl",
"line": 10,
"column": 18,
"byte_offset": 116
}
},
"visibility": "unknown"
},
{
"name": "vcl_recv",
"kind": "function",
"range": {
"start": {
"file": "arrivals.vcl",
"line": 15,
"column": 5,
"byte_offset": 162
},
"end": {
"file": "arrivals.vcl",
"line": 15,
"column": 13,
"byte_offset": 170
}
},
"visibility": "unknown"
},
{
"name": "vcl_backend_response",
"kind": "function",
"range": {
"start": {
"file": "arrivals.vcl",
"line": 28,
"column": 5,
"byte_offset": 432
},
"end": {
"file": "arrivals.vcl",
"line": 28,
"column": 25,
"byte_offset": 452
}
},
"visibility": "unknown"
}
]
}
symbols reports five entries, dropping the version header entirely; transit_api and purge_allowed come back kind: unknown rather than block, while directors, vcl_recv, and vcl_backend_response keep the kinds skeleton reported.
Score the file's branching across both subroutines
vcl_recv branches three times — on the PURGE method, on the purge ACL, and on the arrivals URL — and vcl_backend_response branches once more on that same URL pattern.
$ act query complexity arrivals.vcl
Before
vcl 4.1;
import directors;
backend transit_api {
.host = "10.0.6.20";
.port = "8080";
}
acl purge_allowed {
"127.0.0.1";
"10.0.6.0"/24;
}
sub vcl_recv {
if (req.method == "PURGE") {
if (client.ip !~ purge_allowed) {
return (synth(403, "Forbidden"));
}
return (purge);
}
if (req.url ~ "^/arrivals/") {
unset req.http.Cookie;
}
return (hash);
}
sub vcl_backend_response {
if (bereq.url ~ "^/arrivals/") {
set beresp.ttl = 15s;
}
return (deliver);
}
Output
{
"type": "Complexity",
"score": 5,
"details": "Base complexity: 1, +1 if_statement, +1 if_statement, +1 if_statement, +1 if_statement"
}
The score comes back 5: a base complexity of 1, plus one point for each of the four if_statement nodes in the file — three in vcl_recv, one in vcl_backend_response — combined into a single whole-file score rather than reported per subroutine.
Query
18 query tools, the same on every supported language. Descriptions live in the shared reference: /docs/query-tools.
callers control_flow data_flow definition diagnostics effect_closure effect_summary fix_auto get_type graph import_organize interface mutations references repo_outline skeleton symbols symbols_batch