Skip to content

Commit 7f3763b

Browse files
committed
Report error keys for StdioTransport http variable errors
Use array indices (args[i]) and map keys (env.KEY) instead of generic field paths when reporting http: variable violations in StdioTransport. Added matching validation to the Python implementation and tests for both interpreters.
1 parent 49c15ed commit 7f3763b

File tree

4 files changed

+162
-9
lines changed

4 files changed

+162
-9
lines changed

ballerina-interpreter/parser.bal

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -261,20 +261,18 @@ function validateHttpVariables(AFMRecord afmRecord) returns error? {
261261

262262
string[]? args = transport.args;
263263
if args is string[] {
264-
foreach string arg in args {
265-
if containsHttpVariable(arg) {
266-
erroredKeys.push("tools.mcp.transport.args");
267-
break;
264+
foreach int idx in 0 ..< args.length() {
265+
if containsHttpVariable(args[idx]) {
266+
erroredKeys.push(string `tools.mcp.transport.args[${idx}]`);
268267
}
269268
}
270269
}
271270

272271
map<string>? env = transport.env;
273272
if env is map<string> {
274-
foreach string val in env {
273+
foreach [string, string] [k, val] in env.entries() {
275274
if containsHttpVariable(val) {
276-
erroredKeys.push("tools.mcp.transport.env");
277-
break;
275+
erroredKeys.push("tools.mcp.transport.env." + k);
278276
}
279277
}
280278
}

ballerina-interpreter/tests/main_test.bal

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,75 @@ function testContainsHttpVariable() {
181181
test:assertFalse(containsHttpVariable("no variables here"));
182182
}
183183

184+
@test:Config
185+
function testValidateHttpVariablesInStdioTransportArgs() {
186+
AFMRecord afmRecord = {
187+
metadata: {
188+
spec_version: "0.3.0",
189+
tools: {
190+
mcp: [
191+
{
192+
name: "test-server",
193+
transport: <StdioTransport>{
194+
'type: stdio,
195+
command: "some-command",
196+
args: ["${http:payload.field}", "--safe-arg", "${http:header.auth}"]
197+
}
198+
}
199+
]
200+
}
201+
},
202+
role: "",
203+
instructions: ""
204+
};
205+
206+
error? result = validateHttpVariables(afmRecord);
207+
if result is () {
208+
test:assertFail("Expected error for http: variables in stdio transport args");
209+
}
210+
test:assertTrue(result.message().includes("tools.mcp.transport.args[0]"),
211+
"Expected error to include 'tools.mcp.transport.args[0]'");
212+
test:assertTrue(result.message().includes("tools.mcp.transport.args[2]"),
213+
"Expected error to include 'tools.mcp.transport.args[2]'");
214+
test:assertFalse(result.message().includes("tools.mcp.transport.args[1]"),
215+
"Expected error NOT to include 'tools.mcp.transport.args[1]' (clean arg)");
216+
}
217+
218+
@test:Config
219+
function testValidateHttpVariablesInStdioTransportEnv() {
220+
AFMRecord afmRecord = {
221+
metadata: {
222+
spec_version: "0.3.0",
223+
tools: {
224+
mcp: [
225+
{
226+
name: "test-server",
227+
transport: <StdioTransport>{
228+
'type: stdio,
229+
command: "some-command",
230+
env: {
231+
"CLEAN_VAR": "safe-value",
232+
"SECRET_KEY": "${http:header.Authorization}"
233+
}
234+
}
235+
}
236+
]
237+
}
238+
},
239+
role: "",
240+
instructions: ""
241+
};
242+
243+
error? result = validateHttpVariables(afmRecord);
244+
if result is () {
245+
test:assertFail("Expected error for http: variables in stdio transport env");
246+
}
247+
test:assertTrue(result.message().includes("tools.mcp.transport.env.SECRET_KEY"),
248+
"Expected error to include 'tools.mcp.transport.env.SECRET_KEY'");
249+
test:assertFalse(result.message().includes("tools.mcp.transport.env.CLEAN_VAR"),
250+
"Expected error NOT to include 'tools.mcp.transport.env.CLEAN_VAR' (clean env var)");
251+
}
252+
184253
@test:Config
185254
function testParseAfmWithoutFrontmatter() {
186255
string content = string `# Role

python-interpreter/packages/afm-core/src/afm/variables.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from .models import (
2525
ConsoleChatInterface,
2626
HttpTransport,
27+
StdioTransport,
2728
WebChatInterface,
2829
WebhookInterface,
2930
)
@@ -184,6 +185,17 @@ def validate_http_variables(afm_record: AFMRecord) -> None:
184185
errored_fields.append("tools.mcp.transport.url")
185186
if _auth_contains_http_variable(server.transport.authentication):
186187
errored_fields.append("tools.mcp.transport.authentication")
188+
elif isinstance(server.transport, StdioTransport):
189+
if contains_http_variable(server.transport.command):
190+
errored_fields.append("tools.mcp.transport.command")
191+
if server.transport.args:
192+
for i, arg in enumerate(server.transport.args):
193+
if contains_http_variable(arg):
194+
errored_fields.append(f"tools.mcp.transport.args[{i}]")
195+
if server.transport.env:
196+
for key, value in server.transport.env.items():
197+
if contains_http_variable(value):
198+
errored_fields.append(f"tools.mcp.transport.env.{key}")
187199
if _tool_filter_contains_http_variable(server.tool_filter):
188200
errored_fields.append("tools.mcp.tool_filter")
189201

python-interpreter/packages/afm-core/tests/test_variables.py

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616

1717
import pytest
1818

19-
from afm.exceptions import VariableResolutionError
20-
from afm.variables import resolve_variables
19+
from afm.exceptions import AFMValidationError, VariableResolutionError
20+
from afm.models import AFMRecord, AgentMetadata, MCPServer, StdioTransport, Tools
21+
from afm.variables import resolve_variables, validate_http_variables
2122

2223

2324
class TestResolveVariables:
@@ -55,3 +56,76 @@ def test_skips_commented_variables(self, monkeypatch) -> None:
5556
def test_skips_http_variables(self) -> None:
5657
content = "Payload: ${http:payload.name}"
5758
assert resolve_variables(content) == content
59+
60+
61+
def _make_afm_record_with_stdio_transport(
62+
args: list[str] | None = None,
63+
env: dict[str, str] | None = None,
64+
command: str = "npx",
65+
) -> AFMRecord:
66+
"""Helper to create a minimal AFMRecord with a StdioTransport MCP server."""
67+
return AFMRecord(
68+
metadata=AgentMetadata(
69+
tools=Tools(
70+
mcp=[
71+
MCPServer(
72+
name="test-server",
73+
transport=StdioTransport(
74+
command=command,
75+
args=args,
76+
env=env,
77+
),
78+
)
79+
]
80+
)
81+
),
82+
role="Test role",
83+
instructions="Test instructions",
84+
)
85+
86+
87+
class TestValidateHttpVariablesStdioTransport:
88+
def test_args_reports_individual_indices(self) -> None:
89+
record = _make_afm_record_with_stdio_transport(
90+
args=[
91+
"${http:payload.url}",
92+
"clean-arg",
93+
"${http:header.auth}",
94+
]
95+
)
96+
with pytest.raises(AFMValidationError) as exc_info:
97+
validate_http_variables(record)
98+
msg = str(exc_info.value)
99+
assert "tools.mcp.transport.args[0]" in msg
100+
assert "tools.mcp.transport.args[1]" not in msg
101+
assert "tools.mcp.transport.args[2]" in msg
102+
103+
def test_env_reports_individual_keys(self) -> None:
104+
record = _make_afm_record_with_stdio_transport(
105+
env={
106+
"SECRET_KEY": "${http:payload.secret}",
107+
"CLEAN_VAR": "plain-value",
108+
"API_URL": "${http:payload.api}",
109+
}
110+
)
111+
with pytest.raises(AFMValidationError) as exc_info:
112+
validate_http_variables(record)
113+
msg = str(exc_info.value)
114+
assert "tools.mcp.transport.env.SECRET_KEY" in msg
115+
assert "tools.mcp.transport.env.CLEAN_VAR" not in msg
116+
assert "tools.mcp.transport.env.API_URL" in msg
117+
118+
def test_command_reports_violation(self) -> None:
119+
record = _make_afm_record_with_stdio_transport(command="${http:payload.cmd}")
120+
with pytest.raises(AFMValidationError) as exc_info:
121+
validate_http_variables(record)
122+
assert "tools.mcp.transport.command" in str(exc_info.value)
123+
124+
def test_no_error_when_no_http_variables(self) -> None:
125+
record = _make_afm_record_with_stdio_transport(
126+
command="npx",
127+
args=["--port", "3000"],
128+
env={"HOME": "/home/user"},
129+
)
130+
# Should not raise
131+
validate_http_variables(record)

0 commit comments

Comments
 (0)