Skip to content

Commit 1b936f7

Browse files
authored
Merge pull request #66 from Model-Context-Interface/copilot/support-json-native-resolution
Implement JSON-native resolution for boolean, array, and object properties via {!! ... !!} templating
2 parents 3ee5830 + ca7ce96 commit 1b936f7

File tree

8 files changed

+1422
-9
lines changed

8 files changed

+1422
-9
lines changed

PRD.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,8 @@ Note: Include metadata field for infos like http status code, CLI exit code, etc
322322
Basic templating should be enabled in parts where we have templating such as execution part, headers and etc.
323323
Loops and Control blocks should be applied in large text parts, like text execution and while parsing a file in file execution flow.
324324

325-
- **Basic**: replaces placeholders like `{{props.propertyName}}` and `{{env.VAR_NAME}}` with values.
325+
- **Basic**: replaces placeholders like `{{props.propertyName}}` and `{{env.VAR_NAME}}` with values (always as strings).
326+
- **JSON-Native**: resolves placeholders like `{!!props.propertyName!!}` and `{!!env.VAR_NAME!!}` to native JSON types (boolean, number, array, object, null). Must be the sole value in a field.
326327
- **Loops**: For array and object props or env variables, Adapter should be able to parse `@for` -> `@endfor` and `@foreach` -> `@endforeach`
327328
- **Control Blocks**: Adapter should be able to use control blocks: `@if` -> `@elseif` -> `@else` -> `@endif`
328329

docs/schema_reference.md

Lines changed: 229 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -660,19 +660,36 @@ This behavior prevents template substitution errors for optional properties that
660660
"default": 100
661661
},
662662
"file_extensions": {
663-
"type": "string",
664-
"description": "Optional comma-separated list of file extensions"
663+
"type": "array",
664+
"description": "Optional list of file extensions",
665+
"items": {
666+
"type": "string"
667+
}
665668
}
666669
},
667670
"required": ["pattern", "directory"]
668671
},
669672
"execution": {
670-
"type": "text",
671-
"text": "Searching '{{props.pattern}}' in {{props.directory}} (images: {{props.include_images}}, max: {{props.max_results}})"
673+
"type": "http",
674+
"method": "POST",
675+
"url": "https://api.example.com/search",
676+
"body": {
677+
"type": "json",
678+
"content": {
679+
"pattern": "{{props.pattern}}",
680+
"directory": "{{props.directory}}",
681+
"include_images": "{!!props.include_images!!}",
682+
"case_sensitive": "{!!props.case_sensitive!!}",
683+
"max_results": "{!!props.max_results!!}",
684+
"file_extensions": "{!!props.file_extensions!!}"
685+
}
686+
}
672687
}
673688
}
674689
```
675690

691+
**Note**: Use `{!! ... !!}` syntax for non-string types (boolean, number, array, object) to preserve their native JSON types. See [JSON-Native Placeholders](#json-native-placeholders) for more details.
692+
676693
**Execution with minimal properties:**
677694
```python
678695
# Only required properties provided
@@ -690,10 +707,12 @@ client.execute("search_files", properties={
690707
client.execute("search_files", properties={
691708
"pattern": "FIXME",
692709
"directory": "/tmp",
693-
"include_images": true,
694-
"max_results": 50
710+
"include_images": True,
711+
"max_results": 50,
712+
"file_extensions": [".py", ".js"]
695713
})
696714
# Result: include_images=true, max_results=50 (overridden), case_sensitive=true (default)
715+
# file_extensions=[".py", ".js"] (provided)
697716
```
698717

699718
**Property Resolution Rules:**
@@ -1175,6 +1194,210 @@ Replace placeholders with values from the context.
11751194

11761195
---
11771196

1197+
### JSON-Native Placeholders
1198+
1199+
Resolve placeholders to their native JSON types (boolean, number, array, object, null) instead of strings.
1200+
1201+
**Syntax**: `{!!path.to.value!!}`
1202+
1203+
**Important**: JSON-native placeholders must be the **only** content in a field. They cannot be mixed with other text.
1204+
1205+
#### Supported Types
1206+
1207+
- **Boolean**: `true` or `false` (not `"true"` or `"false"`)
1208+
- **Number**: Integer or float (not string representation)
1209+
- **Array**: Native JSON array (not stringified)
1210+
- **Object**: Native JSON object (not stringified)
1211+
- **Null**: `null` value (not `"null"` string)
1212+
1213+
#### Examples
1214+
1215+
**Boolean Properties**:
1216+
1217+
```json
1218+
{
1219+
"execution": {
1220+
"type": "http",
1221+
"body": {
1222+
"type": "json",
1223+
"content": {
1224+
"include_images": "{!!props.include_images!!}",
1225+
"case_sensitive": "{!!props.case_sensitive!!}"
1226+
}
1227+
}
1228+
}
1229+
}
1230+
```
1231+
1232+
When executed with properties `{"include_images": true, "case_sensitive": false}`, the JSON body will be:
1233+
1234+
```json
1235+
{
1236+
"include_images": true,
1237+
"case_sensitive": false
1238+
}
1239+
```
1240+
1241+
**Array Properties**:
1242+
1243+
```json
1244+
{
1245+
"execution": {
1246+
"type": "http",
1247+
"body": {
1248+
"type": "json",
1249+
"content": {
1250+
"urls": "{!!props.urls!!}",
1251+
"tags": "{!!props.tags!!}"
1252+
}
1253+
}
1254+
}
1255+
}
1256+
```
1257+
1258+
When executed with properties `{"urls": ["https://a.com", "https://b.com"], "tags": ["urgent", "review"]}`, the JSON body will be:
1259+
1260+
```json
1261+
{
1262+
"urls": ["https://a.com", "https://b.com"],
1263+
"tags": ["urgent", "review"]
1264+
}
1265+
```
1266+
1267+
**Object Properties**:
1268+
1269+
```json
1270+
{
1271+
"execution": {
1272+
"type": "http",
1273+
"body": {
1274+
"type": "json",
1275+
"content": {
1276+
"config": "{!!props.config!!}",
1277+
"metadata": "{!!props.metadata!!}"
1278+
}
1279+
}
1280+
}
1281+
}
1282+
```
1283+
1284+
When executed with properties `{"config": {"debug": false, "retries": 3}, "metadata": {"version": "1.0"}}`, the JSON body will be:
1285+
1286+
```json
1287+
{
1288+
"config": {
1289+
"debug": false,
1290+
"retries": 3
1291+
},
1292+
"metadata": {
1293+
"version": "1.0"
1294+
}
1295+
}
1296+
```
1297+
1298+
**Number Properties**:
1299+
1300+
```json
1301+
{
1302+
"execution": {
1303+
"type": "http",
1304+
"body": {
1305+
"type": "json",
1306+
"content": {
1307+
"max_results": "{!!props.max_results!!}",
1308+
"quality": "{!!props.quality!!}"
1309+
}
1310+
}
1311+
}
1312+
}
1313+
```
1314+
1315+
When executed with properties `{"max_results": 100, "quality": 0.95}`, the JSON body will be:
1316+
1317+
```json
1318+
{
1319+
"max_results": 100,
1320+
"quality": 0.95
1321+
}
1322+
```
1323+
1324+
**Mixed Native and String Placeholders**:
1325+
1326+
```json
1327+
{
1328+
"execution": {
1329+
"type": "http",
1330+
"body": {
1331+
"type": "json",
1332+
"content": {
1333+
"enabled": "{!!props.enabled!!}",
1334+
"count": "{!!props.count!!}",
1335+
"name": "{{props.name}}",
1336+
"description": "Search for {{props.query}}"
1337+
}
1338+
}
1339+
}
1340+
}
1341+
```
1342+
1343+
When executed with properties `{"enabled": true, "count": 50, "name": "My Search", "query": "testing"}`, the JSON body will be:
1344+
1345+
```json
1346+
{
1347+
"enabled": true,
1348+
"count": 50,
1349+
"name": "My Search",
1350+
"description": "Search for testing"
1351+
}
1352+
```
1353+
1354+
#### Limitations and Error Cases
1355+
1356+
**✅ Valid Usage**:
1357+
1358+
```json
1359+
{
1360+
"enabled": "{!!props.enabled!!}",
1361+
"items": "{!!props.items!!}",
1362+
"config": "{!!env.CONFIG!!}"
1363+
}
1364+
```
1365+
1366+
**❌ Invalid Usage** (will raise errors):
1367+
1368+
```json
1369+
{
1370+
"message": "Status: {!!props.enabled!!}",
1371+
"url": "https://api.com/{!!props.path!!}",
1372+
"combined": "{!!props.value!!} and more text"
1373+
}
1374+
```
1375+
1376+
**Error Messages**:
1377+
1378+
- **Mixed Content**: "Invalid JSON-native placeholder format: 'text {!!props.value!!}'. Must be exactly {!!path!!} with no surrounding content."
1379+
- **Missing Property**: "Failed to resolve JSON-native placeholder '{!!props.missing!!}': Path 'props.missing' not found in context"
1380+
- **Invalid Syntax**: "Invalid JSON-native placeholder format: '{{props.value}}'. Must be exactly {!!path!!} with no surrounding content."
1381+
1382+
#### When to Use JSON-Native vs String Placeholders
1383+
1384+
**Use `{!! ... !!}` when**:
1385+
1386+
- Property must be a native boolean (`true`/`false`) in JSON
1387+
- Property must be a native number (integer or float) in JSON
1388+
- Property is an array that should remain an array in JSON
1389+
- Property is an object that should remain an object in JSON
1390+
- You need to preserve the exact type from input schema
1391+
1392+
**Use `{{ ... }}` when**:
1393+
1394+
- Building strings with multiple placeholders
1395+
- Concatenating values: `"User {{props.name}} has ID {{props.id}}"`
1396+
- Property should always be a string in the output
1397+
- Using in URLs, headers, or other string-only contexts
1398+
1399+
---
1400+
11781401
### For Loops
11791402

11801403
Iterate a fixed number of times using a range.

src/mcipy/executors/base.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,22 @@ def _apply_basic_templating_to_dict(
153153
"""
154154
Apply basic templating to all string values in a dictionary.
155155
156+
Supports both standard {{...}} placeholders (resolved to strings) and
157+
JSON-native {!!...!!} placeholders (resolved to native types).
158+
156159
Args:
157160
data: Dictionary to process (modified in-place)
158161
context: Context dictionary for template resolution
159162
"""
160163
for key, value in data.items():
161164
if isinstance(value, str):
162-
data[key] = self.template_engine.render_basic(value, context)
165+
# Check if this is a JSON-native placeholder
166+
if self.template_engine.is_json_native_placeholder(value):
167+
# Resolve to native type
168+
data[key] = self.template_engine.resolve_json_native(value, context)
169+
else:
170+
# Standard string templating
171+
data[key] = self.template_engine.render_basic(value, context)
163172
elif isinstance(value, dict):
164173
self._apply_basic_templating_to_dict(value, context)
165174
elif isinstance(value, list):
@@ -169,13 +178,22 @@ def _apply_basic_templating_to_list(self, data: list[Any], context: dict[str, An
169178
"""
170179
Apply basic templating to all string values in a list.
171180
181+
Supports both standard {{...}} placeholders (resolved to strings) and
182+
JSON-native {!!...!!} placeholders (resolved to native types).
183+
172184
Args:
173185
data: List to process (modified in-place)
174186
context: Context dictionary for template resolution
175187
"""
176188
for i, value in enumerate(data):
177189
if isinstance(value, str):
178-
data[i] = self.template_engine.render_basic(value, context)
190+
# Check if this is a JSON-native placeholder
191+
if self.template_engine.is_json_native_placeholder(value):
192+
# Resolve to native type
193+
data[i] = self.template_engine.resolve_json_native(value, context)
194+
else:
195+
# Standard string templating
196+
data[i] = self.template_engine.render_basic(value, context)
179197
elif isinstance(value, dict):
180198
self._apply_basic_templating_to_dict(value, context)
181199
elif isinstance(value, list):

0 commit comments

Comments
 (0)